From 72bae158d54bfb9b407d8f2c224c4ffc4242a7b4 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 23 Jul 2019 20:36:03 +0200 Subject: [PATCH 001/440] docs(guides): add getting started document --- docs/GETTING_STARTED.MD | 72 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 docs/GETTING_STARTED.MD diff --git a/docs/GETTING_STARTED.MD b/docs/GETTING_STARTED.MD new file mode 100644 index 00000000..a5c822e1 --- /dev/null +++ b/docs/GETTING_STARTED.MD @@ -0,0 +1,72 @@ +# Getting Started + +## Dependencies + +--- + +You'll need to have in your Mac the following dependencies installed, if you don't want to use the provided Docker containers. + +* Ruby `2.4.5` +* Rails `5.0.7.1` +* Node `11.12.0` + +## Local Config + +--- + +Instructions for running Development environment using macOS Catalina + +### Initial steps +*Dependencies:* + +Have a ruby version installed, you can learn more about how to use multiple versions of Ruby installed in your computer with [rbenv](https://github.com/rbenv/rbenv) or [rvm](https://rvm.io). + +An instance of PostgresSQL running. + +*Setting up secrets:* + +Run `cp .env.template .env` to copy the provided template file for env variables to create your own. + +You'll need to provide a `DEVISE_SECRET_KEY` and `SECRET_TOKEN` which you can obtain by running `bundle exec rake secret`. + +Set the following secrets in your `.env` file with your *Stripe account* information. + +* `STRIPE_API_KEY` with your Stripe *private* key. +* `STRIPE_API_PUBLIC` with your Stripe *public* key. + +The last secrets you'll need are related to AWS. You can learn how to [create an S3 Bucket](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingBucket.html) within the AWS Documentation, and to obtain your access and secret key, you can [learn more here](https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/). + +* `S3_BUCKET_NAME` +* `AWS_ACCESS_KEY` +* `AWS_SECRET_ACCESS_KEY` + +*Setting up the local database:* + +Run `rake db:setup` to run all the db tasks within one command. This will create the dbs for each environment, load the `structure.sql`, run pending migrations and will also run the seed functionality. + +### How to run +You'll need 2 consoles to run the project. One for the rails env and another one to run the asset pipeline through [webpack](https://webpack.js.org) , since it's *not incorporated yet* into the rails asset pipeline. + +```bash +# Console one (1) +bundle exec rails server +``` + +```bash +# Console two (2) +npm run watch +# #### Notes #### +# If you get errors from running this command. +# You'll need to manually run the following commands. +npm run export-button-config +npm run export-i18n +npm run generate-api-js +# Now we're able to watch! +npx webpack --watch +``` + +## Testing + +--- + +Run `bundle exec rspec` to run test suite. From 9c5f91acb67b52d3027ca57a3240316987c4f857 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 23 Jul 2019 20:38:02 +0200 Subject: [PATCH 002/440] docs(readme): add reference to getting started guide. --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 72937dcd..e63e3a43 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,9 @@ Visit the Internationalization channel on Houdini Zulip and discuss #### Get the code `git clone https://github.com/HoudiniProject/houdini` +#### Mac Setup +If you have a Mac and don't want to run the `docker` configuration, see [how to get started](docs/GETTING_STARTED.MD) with the project. + #### Docker install (if you don't have docker and docker-compose installed) ##### install Docker and Docker compose You need to install Docker and Docker Compose. From bb97427a401a1efe41dd9e5d3b212641ac0e058a Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 26 Oct 2018 11:13:19 -0500 Subject: [PATCH 003/440] We use npm ci only during build test --- script/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test.sh b/script/test.sh index 882f7b2d..285ab5a9 100755 --- a/script/test.sh +++ b/script/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -npm ci --unsafe-perm && rake db:create db:structure:load db:migrate && RAILS_ENV=test rake db:create db:structure:load test:prepare && rake spec && npm run ci-build-all && npx jest \ No newline at end of file +npm ci && rake db:create db:structure:load db:migrate && RAILS_ENV=test rake db:create db:structure:load test:prepare && rake spec && npm run build-all && npx jest From db9493725ec58b0cb59b99c42d58bacd35f5233e Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 2 Nov 2018 15:58:09 -0500 Subject: [PATCH 004/440] FIXED PROBLEM! --- config/environments/ci.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environments/ci.rb b/config/environments/ci.rb index 33e72132..565b75ca 100755 --- a/config/environments/ci.rb +++ b/config/environments/ci.rb @@ -57,4 +57,4 @@ Commitchange::Application.configure do ActiveRecord::Base.logger = nil end -end \ No newline at end of file +end From 3ed8a501e6c4f0db6697a809f3fb941dc0686b0b Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 2 Nov 2018 16:06:27 -0500 Subject: [PATCH 005/440] WWWWWHHHE --- script/test.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/test.sh b/script/test.sh index 285ab5a9..38faac5c 100755 --- a/script/test.sh +++ b/script/test.sh @@ -1,2 +1,2 @@ #!/bin/bash -npm ci && rake db:create db:structure:load db:migrate && RAILS_ENV=test rake db:create db:structure:load test:prepare && rake spec && npm run build-all && npx jest +npm ci && rake db:create db:structure:load db:migrate && RAILS_ENV=test rake db:create db:structure:load test:prepare && rake spec && npm run build-all && npx jest \ No newline at end of file From 168584682881671100df1a27529c1be18f5126df Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 2 Nov 2018 16:23:34 -0500 Subject: [PATCH 006/440] LESS LOGGING --- config/environments/ci.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/ci.rb b/config/environments/ci.rb index 565b75ca..ab04be03 100755 --- a/config/environments/ci.rb +++ b/config/environments/ci.rb @@ -1,5 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Commitchange::Application.configure do +<<<<<<< HEAD # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on From c743fac9761657d22fd325a6405cac873da349ff Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 8 Nov 2018 15:39:21 -0600 Subject: [PATCH 007/440] Finally worky? --- config/environments/ci.rb | 2 -- 1 file changed, 2 deletions(-) diff --git a/config/environments/ci.rb b/config/environments/ci.rb index ab04be03..8787f611 100755 --- a/config/environments/ci.rb +++ b/config/environments/ci.rb @@ -1,6 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Commitchange::Application.configure do -<<<<<<< HEAD # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on @@ -57,5 +56,4 @@ Commitchange::Application.configure do config.after_initialize do ActiveRecord::Base.logger = nil end - end From 3291392c80b17389f22f8f3132a8a2f60c1bc933 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 8 Nov 2018 16:29:53 -0600 Subject: [PATCH 008/440] Boot update --- config/boot.rb | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/config/boot.rb b/config/boot.rb index 3a9aadeb..c6ea5a03 100755 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,8 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'rubygems' - +# # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) -require 'bootsnap/setup' +require 'bootsnap/setup' \ No newline at end of file From 4ea0d48a0137176821a93ab7685c2e18d65b65fb Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 8 Nov 2018 16:37:48 -0600 Subject: [PATCH 009/440] Update inflections --- config/initializers/inflections.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index e42d263f..787a1c49 100755 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,16 +1,17 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. -# Add new inflection rules using the following format -# (all these examples are active by default): -# ActiveSupport::Inflector.inflections do |inflect| +# Add new inflection rules using the following format. Inflections +# are locale specific, and you may define rules for as many different +# locales as you wish. All of these examples are active by default: +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.plural /^(ox)$/i, '\1en' # inflect.singular /^(ox)en/i, '\1' # inflect.irregular 'person', 'people' # inflect.uncountable %w( fish sheep ) # end -# + # These inflection rules are supported but not enabled by default: -# ActiveSupport::Inflector.inflections do |inflect| +# ActiveSupport::Inflector.inflections(:en) do |inflect| # inflect.acronym 'RESTful' # end From 6fa8bd5a627b1934e7fabf21268f0d295dd5aed1 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 8 Nov 2018 16:40:50 -0600 Subject: [PATCH 010/440] Fix wrap params --- config/initializers/wrap_parameters.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 521563e8..97b340a1 100755 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,15 +1,15 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. -# + # This file contains settings for ActionController::ParamsWrapper which # is enabled by default. # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] + wrap_parameters format: [:json] if respond_to?(:wrap_parameters) end -# Disable root element in JSON by default. +# To enable root element in JSON for ActiveRecord objects. ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false + self.include_root_in_json = true end From ca176965293af8c1d6f7cb70876fd06bdb2ce7a7 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 8 Nov 2018 18:29:00 -0600 Subject: [PATCH 011/440] Initial Update to ruby 4.2 --- Gemfile | 11 +- Gemfile.lock | 404 +++++++++--------- .../campaign_gift_options_controller.rb | 40 +- app/controllers/campaigns_controller.rb | 13 +- app/models/nonprofit.rb | 3 +- bin/bundle | 3 + bin/rails | 4 + bin/rake | 4 + config/boot.rb | 5 +- config/environment.rb | 2 +- config/environments/ci.rb | 2 +- config/environments/development.rb | 19 +- config/environments/production.rb | 123 +++--- config/environments/staging.rb | 2 +- config/environments/test.rb | 73 ++-- config/initializers/assets.rb | 8 + config/initializers/cookies_serializer.rb | 3 + .../initializers/filter_parameter_logging.rb | 4 + config/initializers/inflections.rb | 1 - config/initializers/log_rage.rb | 2 +- config/initializers/mime_types.rb | 2 - config/initializers/secret_token.rb | 2 +- config/initializers/session_store.rb | 8 +- config/initializers/timeout.rb | 12 +- config/routes.rb | 58 +-- config/secrets.yml | 22 + db/structure.sql | 2 +- lib/create/create_campaign.rb | 28 ++ lib/email.rb | 2 +- .../api/resource/templates/spec.rb.erb | 2 +- spec/api/houdini/nonprofit_spec.rb | 14 +- .../controllers/campaign_gift_options_spec.rb | 8 +- spec/controllers/campaigns_spec.rb | 25 +- spec/controllers/event_discounts_spec.rb | 6 +- spec/controllers/events_spec.rb | 16 +- .../controllers/nonprofits/activities_spec.rb | 2 +- .../nonprofits/custom_field_masters_spec.rb | 2 +- .../nonprofits/custom_fields_joins_spec.rb | 4 +- spec/controllers/nonprofits/donations_spec.rb | 6 +- spec/controllers/nonprofits/payments_spec.rb | 6 +- spec/controllers/nonprofits/payouts_spec.rb | 2 +- .../nonprofits/recurring_donations_spec.rb | 6 +- .../controllers/nonprofits/supporters_spec.rb | 10 +- spec/controllers/nonprofits/tag_joins_spec.rb | 4 +- .../nonprofits/tag_masters_spec.rb | 2 +- spec/controllers/nonprofits_spec.rb | 35 +- spec/controllers/profiles_spec.rb | 2 +- spec/controllers/recurring_donations_spec.rb | 10 +- spec/controllers/roles_spec.rb | 2 +- spec/controllers/ticket_levels_spec.rb | 10 +- spec/lib/create/create_campaign_spec.rb | 8 + spec/lib/query/query_donations_spec.rb | 2 +- 52 files changed, 559 insertions(+), 487 deletions(-) create mode 100755 bin/bundle create mode 100755 bin/rails create mode 100755 bin/rake create mode 100644 config/initializers/assets.rb create mode 100644 config/initializers/cookies_serializer.rb create mode 100644 config/initializers/filter_parameter_logging.rb create mode 100644 config/secrets.yml create mode 100644 spec/lib/create/create_campaign_spec.rb diff --git a/Gemfile b/Gemfile index abe7d29a..647c0c7f 100755 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' ruby '2.3.7' gem 'rake' -gem 'rails', '3.2.22.5' +gem 'rails', '~> 4.1' gem 'rails_12factor' # https://stripe.com/docs/api gem 'stripe' @@ -26,8 +26,7 @@ gem 'test-unit', '~> 3.0' gem 'hamster' gem 'aws-ses' -gem 'aws-sdk' - +gem 'aws-sdk', '~> 1' # for blocking ip addressses gem 'rack-attack' @@ -91,7 +90,7 @@ gem 'table_print' gem 'bunny', '>= 2.6.3' -gem 'rails-i18n', '~> 3.0.0' # For 3.x +gem 'rails-i18n', '~> 4.0' # For 3.x gem 'i18n-js' gem 'countries' @@ -125,7 +124,7 @@ end # Gems used for asset compilation gem 'sass', '3.2.19' -gem 'sass-rails', '3.2.6' +gem 'sass-rails' gem 'uglifier' # make logging less terrible in rails @@ -137,7 +136,7 @@ gem 'dry-validation' # used only for config validation gem 'foreman' gem 'grape', '~> 1.1.0' -gem 'grape-entity', git: 'https://github.com/ruby-grape/grape-entity.git', ref: '0e04aa561373b510c2486282979085eaef2ae663' +gem 'grape-entity' gem 'grape-swagger' gem 'grape-swagger-entity' gem 'grape_url_validator' diff --git a/Gemfile.lock b/Gemfile.lock index 113acbf7..aa61483f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -13,9 +13,9 @@ GIT revision: 0e04aa561373b510c2486282979085eaef2ae663 ref: 0e04aa561373b510c2486282979085eaef2ae663 specs: - grape-entity (0.7.1) - activesupport (>= 3.0.0) - multi_json (>= 1.3.2) + qx (0.1.1) + activerecord (>= 3.0) + colorize (~> 0.8) PATH remote: gems/grape_devise @@ -42,100 +42,108 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.0.0) - actionmailer (3.2.22.5) - actionpack (= 3.2.22.5) - mail (~> 2.5.4) - actionpack (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - builder (~> 3.0.0) - erubis (~> 2.7.0) - journey (~> 1.0.4) - rack (~> 1.4.5) - rack-cache (~> 1.2) - rack-test (~> 0.6.1) - sprockets (~> 2.2.1) - activemodel (3.2.22.5) - activesupport (= 3.2.22.5) - builder (~> 3.0.0) - activerecord (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - arel (~> 3.0.2) - tzinfo (~> 0.3.29) - activeresource (3.2.22.5) - activemodel (= 3.2.22.5) - activesupport (= 3.2.22.5) - activesupport (3.2.22.5) + actionmailer (4.1.16) + actionpack (= 4.1.16) + actionview (= 4.1.16) + mail (~> 2.5, >= 2.5.4) + actionpack (4.1.16) + actionview (= 4.1.16) + activesupport (= 4.1.16) + rack (~> 1.5.2) + rack-test (~> 0.6.2) + activemodel (4.0.0) + activesupport (= 4.0.0) + builder (~> 3.1.0) + activerecord (4.0.0) + activemodel (= 4.0.0) + activerecord-deprecated_finders (~> 1.0.2) + activesupport (= 4.0.0) + arel (~> 4.0.0) + activerecord-deprecated_finders (1.0.4) + activesupport (4.0.0) i18n (~> 0.6, >= 0.6.4) multi_json (~> 1.0) addressable (2.3.8) amq-protocol (2.2.0) andand (1.3.3) - arel (3.0.3) - aws-sdk (1.66.0) - aws-sdk-v1 (= 1.66.0) - aws-sdk-v1 (1.66.0) + arel (5.0.1.20140414130214) + aws-eventstream (1.0.1) + aws-partitions (1.110.0) + aws-sdk (1.67.0) + aws-sdk-v1 (= 1.67.0) + aws-sdk-core (3.37.0) + aws-eventstream (~> 1.0) + aws-partitions (~> 1.0) + aws-sigv4 (~> 1.0) + jmespath (~> 1.0) + aws-sdk-kms (1.11.0) + aws-sdk-core (~> 3, >= 3.26.0) + aws-sigv4 (~> 1.0) + aws-sdk-s3 (1.23.1) + aws-sdk-core (~> 3, >= 3.26.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.0) + aws-sdk-v1 (1.67.0) json (~> 1.4) - nokogiri (>= 1.4.4) + nokogiri (~> 1) aws-ses (0.6.0) builder mail (> 2.2.5) mime-types xml-simple + aws-sigv4 (1.0.3) axiom-types (0.1.1) descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) - bcrypt (3.1.11) - binding_of_caller (0.7.2) + bcrypt (3.1.12) + binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) - bootsnap (1.1.7) + bootsnap (1.3.2) msgpack (~> 1.0) - browserify-rails (0.9.3) - sprockets (~> 2.2) - builder (3.0.4) - bunny (2.7.1) - amq-protocol (>= 2.2.0) - carrierwave (0.10.0) - activemodel (>= 3.2.0) - activesupport (>= 3.2.0) - json (>= 1.7) + browserify-rails (1.1.0) + railties (>= 4.0.0, < 5.0) + builder (3.2.3) + bunny (2.12.0) + amq-protocol (~> 2.3, >= 2.3.0) + carrierwave (1.2.3) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) mime-types (>= 1.16) - carrierwave-aws (0.5.0) - aws-sdk (~> 1.58) - carrierwave (~> 0.7) + carrierwave-aws (1.3.0) + aws-sdk-s3 (~> 1.0) + carrierwave (>= 0.7, < 2.0) chronic (0.10.2) coderay (1.1.2) coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.8.1) - concurrent-ruby (1.0.5) + concurrent-ruby (1.1.4) config (1.7.0) activesupport (>= 3.0) deep_merge (~> 1.2.1) dry-validation (>= 0.10.4) - countries (2.1.2) + countries (2.1.4) i18n_data (~> 0.8.0) money (~> 6.9) sixarm_ruby_unaccent (~> 1.1) unicode_utils (~> 1.4) - crack (0.4.2) + crack (0.4.3) safe_yaml (~> 1.0.0) - css_parser (1.3.6) + css_parser (1.6.0) addressable - dalli (2.7.6) + dalli (2.7.9) dante (0.2.0) - database_cleaner (1.6.1) + database_cleaner (1.7.0) debase (0.2.2) debase-ruby_core_source (>= 0.10.2) debase-ruby_core_source (0.10.3) - debug_inspector (0.0.2) + debug_inspector (0.0.3) deep_merge (1.2.1) - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) - delayed_job_active_record (4.1.1) - activerecord (>= 3.0, < 5.1) + delayed_job (4.1.5) + activesupport (>= 3.0, < 5.3) + delayed_job_active_record (4.1.3) + activerecord (>= 3.0, < 5.3) delayed_job (>= 3.0, < 5) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) @@ -146,65 +154,65 @@ GEM responders thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) - diff-lcs (1.2.5) + devise-async (0.10.2) + devise (>= 3.2, < 4.0) + diff-lcs (1.3) docile (1.3.1) - domain_name (0.5.20160615) + domain_name (0.5.20180417) unf (>= 0.0.5, < 1.0.0) - dotenv (2.0.1) - dotenv-rails (2.0.1) - dotenv (= 2.0.1) + dotenv (2.5.0) + dotenv-rails (2.5.0) + dotenv (= 2.5.0) + railties (>= 3.2, < 6.0) dry-configurable (0.7.0) concurrent-ruby (~> 1.0) dry-container (0.6.0) concurrent-ruby (~> 1.0) dry-configurable (~> 0.1, >= 0.1.3) - dry-core (0.4.5) + dry-core (0.4.7) concurrent-ruby (~> 1.0) - dry-equalizer (0.2.0) + dry-equalizer (0.2.1) + dry-inflector (0.1.2) dry-logic (0.4.2) dry-container (~> 0.2, >= 0.2.6) dry-core (~> 0.2) dry-equalizer (~> 0.2) - dry-types (0.12.2) + dry-types (0.13.2) concurrent-ruby (~> 1.0) - dry-configurable (~> 0.1) dry-container (~> 0.3) - dry-core (~> 0.2, >= 0.2.1) + dry-core (~> 0.4, >= 0.4.4) dry-equalizer (~> 0.2) + dry-inflector (~> 0.1, >= 0.1.2) dry-logic (~> 0.4, >= 0.4.2) - inflecto (~> 0.0.0, >= 0.0.2) - dry-validation (0.11.1) + dry-validation (0.12.2) concurrent-ruby (~> 1.0) dry-configurable (~> 0.1, >= 0.1.3) dry-core (~> 0.2, >= 0.2.1) dry-equalizer (~> 0.2) dry-logic (~> 0.4, >= 0.4.0) - dry-types (~> 0.12.0) + dry-types (~> 0.13.1) equalizer (0.0.11) erubis (2.7.0) - execjs (2.5.2) - factory_bot (4.8.2) + execjs (2.7.0) + factory_bot (4.11.1) activesupport (>= 3.0.0) - factory_bot_rails (4.8.2) - factory_bot (~> 4.8.2) + factory_bot_rails (4.11.1) + factory_bot (~> 4.11.1) railties (>= 3.0.0) - faraday (0.9.1) + faraday (0.11.0) multipart-post (>= 1.2, < 3) faraday_middleware (0.9.1) faraday (>= 0.7.4, < 0.10) font_assets (0.1.14) rack - foreman (0.84.0) + foreman (0.85.0) thor (~> 0.19.1) - fullcontact (0.9.0) - faraday (~> 0.9.0) - faraday_middleware (>= 0.9) + fullcontact (0.18.0) + faraday (~> 0.11.0) + faraday_middleware (>= 0.10) hashie (>= 2.0, < 4.0) - plissken - geocoder (1.2.11) - get_process_mem (0.2.1) + geocoder (1.5.0) + get_process_mem (0.2.3) grape (1.1.0) activesupport builder @@ -212,11 +220,14 @@ GEM rack (>= 1.3.0) rack-accept virtus (>= 1.0.0) - grape-swagger (0.28.0) + grape-entity (0.7.1) + activesupport (>= 4.0) + multi_json (>= 1.3.2) + grape-swagger (0.31.1) grape (>= 0.16.2) - grape-swagger-entity (0.2.3) + grape-swagger-entity (0.3.0) grape-entity (>= 0.5.0) - grape-swagger (>= 0.20.4) + grape-swagger (>= 0.31.0) grape_logging (1.8.0) grape rack @@ -224,36 +235,34 @@ GEM grape (>= 0.12.0) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashie (3.4.1) - heroku-deflater (0.5.3) + hashdiff (0.3.7) + hashie (3.6.0) + heroku-deflater (0.6.3) rack (>= 1.4.5) - hike (1.2.3) - http-cookie (1.0.2) + http-cookie (1.0.3) domain_name (~> 0.5) - httparty (0.13.3) - json (~> 1.8) + httparty (0.16.2) multi_xml (>= 0.5.2) i18n (0.9.5) concurrent-ruby (~> 1.0) - i18n-js (3.0.2) - i18n (~> 0.6, >= 0.6.6) + i18n-js (3.1.0) + i18n (>= 0.6.6, < 2) i18n_data (0.8.0) ice_nine (0.11.2) - inflecto (0.0.2) - journey (1.0.4) + jmespath (1.4.0) json (1.8.6) - kdtree (0.3) - lograge (0.3.6) - actionpack (>= 3) - activesupport (>= 3) - railties (>= 3) - mail (2.5.5) - mime-types (~> 1.16) - treetop (~> 1.4.8) + kdtree (0.4) + lograge (0.10.0) + actionpack (>= 4) + activesupport (>= 4) + railties (>= 4) + request_store (~> 1.0) + mail (2.7.1) + mini_mime (>= 0.1.1) mail_view (2.0.4) tilt memcachier (0.0.2) - method_source (0.9.0) + method_source (0.9.1) mime-types (1.25.1) mini_magick (4.9.5) mini_portile2 (2.1.0) @@ -261,7 +270,7 @@ GEM i18n (>= 0.6.4, < 1.0) msgpack (1.2.0) multi_json (1.13.1) - multi_xml (0.5.5) + multi_xml (0.6.0) multipart-post (2.0.0) mustermann (1.0.3) mustermann-grape (1.0.0) @@ -271,47 +280,46 @@ GEM kdtree require_all netrc (0.11.0) - nokogiri (1.6.8.1) - mini_portile2 (~> 2.1.0) + nokogiri (1.8.5) + mini_portile2 (~> 2.3.0) orm_adapter (0.5.0) - parallel (1.6.1) - pg (0.18.3) - plissken (0.2.0) - symbolize (~> 4.2) + parallel (1.12.1) + pg (0.21.0) polyglot (0.3.5) power_assert (1.1.1) pry (0.11.3) coderay (~> 1.1.0) method_source (~> 0.9.0) - puma (3.11.2) + public_suffix (3.0.3) + puma (3.12.0) puma_worker_killer (0.1.0) get_process_mem (~> 0.2) puma (>= 2.7, < 4) - rabl (0.11.6) + rabl (0.14.0) activesupport (>= 2.3.14) - rack (1.4.7) + rack (1.5.5) rack-accept (0.4.5) rack (>= 0.4) - rack-attack (4.2.0) - rack - rack-cache (1.7.2) - rack (>= 0.4) - rack-ssl (1.3.4) + rack-attack (5.4.2) + rack (>= 1.0, < 3) + rack-ssl (1.4.1) rack rack-test (0.6.3) rack (>= 1.0) - rack-timeout (0.4.2) - rails (3.2.22.5) - actionmailer (= 3.2.22.5) - actionpack (= 3.2.22.5) - activerecord (= 3.2.22.5) - activeresource (= 3.2.22.5) - activesupport (= 3.2.22.5) - bundler (~> 1.0) - railties (= 3.2.22.5) - rails-i18n (3.0.1) - i18n (~> 0.5) - rails (>= 3.0.0, < 4.0.0) + rack-timeout (0.5.1) + rails (4.1.16) + actionmailer (= 4.1.16) + actionpack (= 4.1.16) + actionview (= 4.1.16) + activemodel (= 4.1.16) + activerecord (= 4.1.16) + activesupport (= 4.1.16) + bundler (>= 1.3.0, < 2.0) + railties (= 4.1.16) + sprockets-rails (~> 2.0) + rails-i18n (4.0.9) + i18n (~> 0.7) + railties (~> 4.0) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging @@ -322,90 +330,86 @@ GEM activesupport (= 3.2.22.5) rack-ssl (~> 1.3.2) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (12.3.1) - rdoc (3.12.2) - json (~> 1.4) - require_all (1.3.2) + thor (>= 0.18.1, < 2.0) + rake (12.3.2) + request_store (1.4.1) + rack (>= 1.4) + require_all (2.0.0) responders (1.1.2) railties (>= 3.2, < 4.2) - rest-client (1.8.0) + rest-client (2.0.2) http-cookie (>= 1.0.2, < 2.0) - mime-types (>= 1.16, < 3.0) - netrc (~> 0.7) - roadie (3.0.4) - css_parser (~> 1.3.4) - nokogiri (~> 1.6.0) - roadie-rails (1.0.5) - railties (>= 3.0, < 4.3) - roadie (~> 3.0) - rspec (3.5.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-core (3.5.1) - rspec-support (~> 3.5.0) - rspec-expectations (3.5.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) + roadie (3.4.0) + css_parser (~> 1.4) + nokogiri (~> 1.5) + roadie-rails (1.3.0) + railties (>= 3.0, < 5.3) + roadie (~> 3.1) + rspec (3.8.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-core (3.8.0) + rspec-support (~> 3.8.0) + rspec-expectations (3.8.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.8.0) + rspec-mocks (3.8.0) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.0) + rspec-support (~> 3.8.0) + rspec-rails (3.8.1) actionpack (>= 3.0) activesupport (>= 3.0) railties (>= 3.0) - rspec-core (~> 3.5.0) - rspec-expectations (~> 3.5.0) - rspec-mocks (~> 3.5.0) - rspec-support (~> 3.5.0) - rspec-support (3.5.0) + rspec-core (~> 3.8.0) + rspec-expectations (~> 3.8.0) + rspec-mocks (~> 3.8.0) + rspec-support (~> 3.8.0) + rspec-support (3.8.0) ruby-debug-ide (0.6.1) rake (>= 0.8.1) ruby-prof (0.15.9) safe_yaml (1.0.4) sass (3.2.19) - sass-rails (3.2.6) - railties (~> 3.2.0) - sass (>= 3.1.10) - tilt (~> 1.3) + sass-rails (5.0.7) + railties (>= 4.0.0, < 6) + sass (~> 3.1) + sprockets (>= 2.8, < 4.0) + sprockets-rails (>= 2.0, < 4.0) + tilt (>= 1.1, < 3) simplecov (0.16.1) docile (~> 1.1) json (>= 1.8, < 3) simplecov-html (~> 0.10.0) simplecov-html (0.10.2) sixarm_ruby_unaccent (1.2.0) - sprockets (2.2.3) - hike (~> 1.2) - multi_json (~> 1.0) - rack (~> 1.0) - tilt (~> 1.1, != 1.3.0) - stripe (1.49.0) - rest-client (>= 1.4, < 3.0) - symbolize (4.5.2) - activemodel (>= 3.2, < 5) - activesupport (>= 3.2, < 5) - i18n - table_print (1.5.4) - test-unit (3.2.7) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (2.3.3) + actionpack (>= 3.0) + activesupport (>= 3.0) + sprockets (>= 2.8, < 4.0) + stripe (1.58.0) + rest-client (>= 1.4, < 4.0) + table_print (1.5.6) + test-unit (3.2.8) power_assert thor (0.19.4) thread_safe (0.3.6) - tilt (1.4.1) - timecop (0.7.3) - traceroute (0.5.0) + tilt (2.0.9) + timecop (0.9.1) + traceroute (0.8.0) rails (>= 3.0.0) - treetop (1.4.15) - polyglot - polyglot (>= 0.3.1) - tzinfo (0.3.54) - uglifier (2.7.1) - execjs (>= 0.3.0) - json (>= 1.8.0) + tzinfo (1.2.5) + thread_safe (~> 0.1) + uglifier (4.1.19) + execjs (>= 0.3.0, < 3) unf (0.1.4) unf_ext - unf_ext (0.0.7.2) + unf_ext (0.0.7.5) unicode_utils (1.4.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -414,9 +418,10 @@ GEM equalizer (~> 0.0, >= 0.0.9) warden (1.2.7) rack (>= 1.0) - webmock (1.21.0) + webmock (3.4.2) addressable (>= 2.3.6) crack (>= 0.3.2) + hashdiff xml-simple (1.1.5) PLATFORMS @@ -451,7 +456,7 @@ DEPENDENCIES fullcontact geocoder grape (~> 1.1.0) - grape-entity! + grape-entity grape-swagger grape-swagger-entity grape_devise! @@ -475,9 +480,10 @@ DEPENDENCIES qx! rabl rack-attack + rack-ssl rack-timeout - rails (= 3.2.22.5) - rails-i18n (~> 3.0.0) + rails (~> 4.1) + rails-i18n (~> 4.0) rails_12factor rake roadie-rails @@ -486,7 +492,7 @@ DEPENDENCIES ruby-debug-ide ruby-prof (= 0.15.9) sass (= 3.2.19) - sass-rails (= 3.2.6) + sass-rails simplecov (~> 0.16.1) sprockets stripe diff --git a/app/controllers/campaigns/campaign_gift_options_controller.rb b/app/controllers/campaigns/campaign_gift_options_controller.rb index e413855f..83e5da31 100644 --- a/app/controllers/campaigns/campaign_gift_options_controller.rb +++ b/app/controllers/campaigns/campaign_gift_options_controller.rb @@ -2,9 +2,9 @@ module Campaigns; class CampaignGiftOptionsController < ApplicationController include Controllers::CampaignHelper - before_filter :authenticate_campaign_editor!, only: [:index] + before_filter :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order, :report] - def index + def report respond_to do |format| format.json do render json: QueryCampaignGifts.report_metrics(current_campaign.id) @@ -12,4 +12,40 @@ module Campaigns; class CampaignGiftOptionsController < ApplicationController end end + + + def index + @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') + render json: {data: @gift_options} + end + + def show + render json: {data: current_campaign.campaign_gift_options.find(params[:id])} + end + + def create + campaign = current_campaign + json_saved CreateCampaignGiftOption.create(campaign, params[:campaign_gift_option]), + 'Gift option successfully created!' + end + + def update + @campaign = current_campaign + gift_option = @campaign.campaign_gift_options.find params[:id] + json_saved UpdateCampaignGiftOption.update(gift_option, params[:campaign_gift_option]), 'Successfully updated' + end + + # put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order + # Pass in {data: [{id: 1, order: 1}]} + def update_order + updated_gift_options = UpdateOrder.with_data('campaign_gift_options', params[:data]) + render json: updated_gift_options + end + + def destroy + @campaign = current_campaign + + render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id])} + end + end; end diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index 861370a7..03e95d51 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -57,18 +57,7 @@ class CampaignsController < ApplicationController end def create - Time.use_zone(current_nonprofit.timezone || 'UTC') do - params[:campaign][:end_datetime] = Chronic.parse(params[:campaign][:end_datetime]) if params[:campaign][:end_datetime].present? - end - - if !params[:campaign][:parent_campaign_id] - campaign = current_nonprofit.campaigns.create params[:campaign] - json_saved campaign, 'Campaign created! Well done.' - else - profile_id = params[:campaign][:profile_id] - Profile.find(profile_id).update_attributes params[:profile] - render json: CreatePeerToPeerCampaign.create(params[:campaign], profile_id) - end + render json: CreateCampaign.create(params, current_nonprofit) end def update diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index 421b59b1..f7280036 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -73,7 +73,8 @@ class Nonprofit < ActiveRecord::Base has_many :email_settings has_many :cards, as: :holder - has_one :bank_account, dependent: :destroy, conditions: "COALESCE(deleted, false) = false" + has_one :bank_account, -> { where("COALESCE(deleted, false) = false") }, + dependent: :destroy has_one :billing_subscription, dependent: :destroy has_one :billing_plan, through: :billing_subscription has_one :miscellaneous_np_info diff --git a/bin/bundle b/bin/bundle new file mode 100755 index 00000000..66e9889e --- /dev/null +++ b/bin/bundle @@ -0,0 +1,3 @@ +#!/usr/bin/env ruby +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails new file mode 100755 index 00000000..728cd85a --- /dev/null +++ b/bin/rails @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +APP_PATH = File.expand_path('../../config/application', __FILE__) +require_relative '../config/boot' +require 'rails/commands' diff --git a/bin/rake b/bin/rake new file mode 100755 index 00000000..17240489 --- /dev/null +++ b/bin/rake @@ -0,0 +1,4 @@ +#!/usr/bin/env ruby +require_relative '../config/boot' +require 'rake' +Rake.application.run diff --git a/config/boot.rb b/config/boot.rb index c6ea5a03..b1a8cf10 100755 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,7 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -# # Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) +require 'bundler/setup' require 'bootsnap/setup' \ No newline at end of file diff --git a/config/environment.rb b/config/environment.rb index 8b283da0..32a0ed66 100755 --- a/config/environment.rb +++ b/config/environment.rb @@ -298,4 +298,4 @@ end Settings.reload! # Initialize the rails application -Commitchange::Application.initialize! +Rails.application.initialize! diff --git a/config/environments/ci.rb b/config/environments/ci.rb index 8787f611..dc55b812 100755 --- a/config/environments/ci.rb +++ b/config/environments/ci.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on diff --git a/config/environments/development.rb b/config/environments/development.rb index 775ab196..eb0ad2c7 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -5,7 +5,7 @@ CarrierWave.configure do |config| config.ignore_download_errors = false end -Commitchange::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # In the development environment your application's code is reloaded on @@ -14,9 +14,8 @@ Commitchange::Application.configure do config.cache_classes = false config.cache_store = Settings.default.cache_store.to_sym - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true - + # Do not eager load code on boot. + config.eager_load = false # Show full error reports and disable caching config.consider_all_requests_local = true config.action_controller.perform_caching = false @@ -26,11 +25,12 @@ Commitchange::Application.configure do # config.action_mailer.default_url_options = { host: 'commitchange.com' } config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } - config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username - config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password + config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username + config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password config.action_mailer.default_url_options = { host: Settings.mailer.host } - + # Don't care if the mailer can't send. + config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the Rails logger config.active_support.deprecation = :log @@ -40,9 +40,8 @@ Commitchange::Application.configure do # Raise exception on mass assignment protection for Active Record models config.active_record.mass_assignment_sanitizer = :strict - # Log the query plan for queries taking more than this (works) - # with SQLite, MySQL, and PostgreSQL) - config.active_record.auto_explain_threshold_in_seconds = 0.5 + # Raise an error on page load if there are pending migrations + config.active_record.migration_error = :page_load # Do not compress assets config.assets.compress = false diff --git a/config/environments/production.rb b/config/environments/production.rb index 8de071ba..b93a877c 100755 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,89 +1,78 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.configure do - # Settings specified here will take precedence over those in config/application.rb +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. - # Code is not reloaded between requests - config.cache_classes = true - config.cache_store = Settings.default.cache_store.to_sym + # Code is not reloaded between requests. + config.cache_classes = true - # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false - config.action_controller.perform_caching = true + # Eager load code on boot. This eager loads most of Rails and + # your application in memory, allowing both threaded web servers + # and those relying on copy on write to perform better. + # Rake tasks automatically ignore this option for performance. + config.eager_load = true - # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = true + # Full error reports are disabled and caching is turned on. + config.consider_all_requests_local = false + config.action_controller.perform_caching = true - # Compress JavaScripts and CSS - config.assets.compress = true + # Enable Rack::Cache to put a simple HTTP cache in front of your application + # Add `rack-cache` to your Gemfile before enabling this. + # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. + # config.action_dispatch.rack_cache = true - # Generate digests for assets URLs - config.assets.digest = true + # Disable Rails's static asset server (Apache or nginx will already do this). + config.serve_static_assets = false + + # Compress JavaScripts and CSS. + config.assets.js_compressor = :uglifier + # config.assets.css_compressor = :sass + + # Do not fallback to assets pipeline if a precompiled asset is missed. config.assets.compile = false - # Defaults to nil and saved in location specified by config.assets.prefix - # config.assets.manifest = YOUR_PATH + # Generate digests for assets URLs. + config.assets.digest = true - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true + # Specifies the header that your server uses for sending files. + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - # See everything in the log (default is :info) - config.log_level = :debug + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + # config.force_ssl = true - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] + # Set to :debug to see everything in the log. + config.log_level = :info - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # Prepend all log lines with the following tags. + # config.log_tags = [ :subdomain, :uuid ] - # Use a different cache store in production - # config.cache_store = :mem_cache_store + # Use a different logger for distributed setups. + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - # Enable serving of images, stylesheets, and JavaScripts from an asset server + # Use a different cache store in production. + # config.cache_store = :mem_cache_store - cdn_url= URI(Settings.cdn.url) - cdn_url = cdn_url.to_s - config.action_controller.asset_host = cdn_url - config.action_mailer.asset_host = cdn_url - config.font_assets.origin = '*' + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = "http://assets.example.com" - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # Ignore bad email addresses and do not raise email delivery errors. + # Set this to true and configure the email server for immediate delivery to raise delivery errors. + # config.action_mailer.raise_delivery_errors = false - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym - config.action_mailer.default_url_options = { host: Settings.mailer.host } - # Precompile all "page" files, it needs to be set here so the proper env is setup - config.assets.precompile << Proc.new do |path| - if path =~ /.*page\.(css|js)/ - puts "Compiling asset: " + path - true - else - false - end - end + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation cannot be found). + config.i18n.fallbacks = true - # Enable threaded mode - # config.threadsafe! + # Send deprecation notices to registered listeners. + config.active_support.deprecation = :notify - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true + # Disable automatic flushing of the log to improve performance. + # config.autoflush_log = false - # Send deprecation notices to registered listeners - config.active_support.deprecation = :notify + # Use default logging formatter so that PID and timestamp are not suppressed. + config.log_formatter = ::Logger::Formatter.new - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - # config.active_record.auto_explain_threshold_in_seconds = 0.5 - - config.assets.compile = false - - config.threadsafe! - config.dependency_loading = true if $rails_rake_task - # Compress json - # config.middleware.use Rack::Deflater + # Do not dump schema after migrations. + config.active_record.dump_schema_after_migration = false end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 19c31c8d..d9717aaa 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb # Code is not reloaded between requests diff --git a/config/environments/test.rb b/config/environments/test.rb index 64bf4084..48544a21 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,55 +1,42 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.configure do - # Settings specified here will take precedence over those in config/application.rb +Rails.application.configure do + # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.cache_classes = false + # The test environment is used exclusively to run your application's + # test suite. You never need to work with it otherwise. Remember that + # your test database is "scratch space" for the test suite and is wiped + # and recreated between test runs. Don't rely on the data there! + config.cache_classes = true - # Log error messages when you accidentally call methods on nil. - config.whiny_nils = true + # Do not eager load code on boot. This avoids loading your whole application + # just for the purpose of running a single test. If you are using a tool that + # preloads Rails for running tests, you may have to set it to true. + config.eager_load = false - # Show full error reports and disable caching + # Configure static asset server for tests with Cache-Control for performance. + config.serve_static_assets = true + config.static_cache_control = 'public, max-age=3600' + + # Show full error reports and disable caching. config.consider_all_requests_local = true - # config.action_controller.perform_caching = false + config.action_controller.perform_caching = false - # Don't care if the mailer can't send - config.action_mailer.delivery_method = :test - config.action_mailer.raise_delivery_errors = false - config.action_mailer.default_url_options = { host: 'localhost:8080' } - - # Print deprecation notices to the Rails logger - config.active_support.deprecation = :log - - # Only use best-standards-support built into browsers - config.action_dispatch.best_standards_support = :builtin - - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict - - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - config.active_record.auto_explain_threshold_in_seconds = 0.5 - - # Do not compress assets - config.assets.compress = false - - # Expands the lines which load the assets - config.assets.debug = true - - config.log_level = :none + # Raise exceptions instead of rendering exception templates. + config.action_dispatch.show_exceptions = false + # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false - config.cache_store = :memory_store - config.threadsafe! - config.after_initialize do - ActiveRecord::Base.logger = nil - ActionController::Base.logger = nil - ActionMailer::Base.logger = nil - end + # Tell Action Mailer not to deliver emails to the real world. + # The :test delivery method accumulates sent emails in the + # ActionMailer::Base.deliveries array. + config.action_mailer.delivery_method = :test + + # Print deprecation notices to the stderr. + config.active_support.deprecation = :stderr + config.action_mailer.default_url_options = {host: 'houdiniproject.test'} + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb new file mode 100644 index 00000000..d2f4ec33 --- /dev/null +++ b/config/initializers/assets.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# Version of your assets, change this if you want to expire all your assets. +Rails.application.config.assets.version = '1.0' + +# Precompile additional assets. +# application.js, application.css, and all non-JS/CSS in app/assets folder are already added. +# Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb new file mode 100644 index 00000000..7a06a89f --- /dev/null +++ b/config/initializers/cookies_serializer.rb @@ -0,0 +1,3 @@ +# Be sure to restart your server when you modify this file. + +Rails.application.config.action_dispatch.cookies_serializer = :json \ No newline at end of file diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb new file mode 100644 index 00000000..4a994e1e --- /dev/null +++ b/config/initializers/filter_parameter_logging.rb @@ -0,0 +1,4 @@ +# Be sure to restart your server when you modify this file. + +# Configure sensitive parameters which will be filtered from the log file. +Rails.application.config.filter_parameters += [:password] diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index 787a1c49..ac033bf9 100755 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,4 +1,3 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/log_rage.rb b/config/initializers/log_rage.rb index 5e8470d6..f76666a7 100644 --- a/config/initializers/log_rage.rb +++ b/config/initializers/log_rage.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.configure do +Rails.application.configure do if (Rails.env != 'test') diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index 2f890f79..dc189968 100755 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,6 +1,4 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: # Mime::Type.register "text/richtext", :rtf -# Mime::Type.register_alias "text/html", :iphone diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index fb63eadd..10dc8dd1 100755 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -5,4 +5,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Commitchange::Application.config.secret_token = ENV.fetch('SECRET_TOKEN') +Rails.application.config.secret_token = ENV.fetch('SECRET_TOKEN') diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 9c08e52a..0fa3b506 100755 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,9 +1,3 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. -Commitchange::Application.config.session_store ActionDispatch::Session::CacheStore, :expire_after => 4.hours - -# Use the database for sessions instead of the cookie-based default, -# which shouldn't be used to store highly confidential information -# (create the session table with "rails generate session_migration") -# Commitchange::Application.config.session_store :active_record_store +Rails.application.config.session_store :cookie_store, key: '_commitchange_session' diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb index 41aab386..e1cc5c7c 100644 --- a/config/initializers/timeout.rb +++ b/config/initializers/timeout.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -timeout = Integer(ENV['WEB_TIMEOUT'] || 15) -if ENV['RAILS_ENV'] == 'development' || ENV['IDE_PROCESS_DISPATCHER'] - timeout = 10000 -end - -Rack::Timeout.timeout = timeout # seconds \ No newline at end of file +# timeout = Integer(ENV['WEB_TIMEOUT'] || 15) +# if ENV['RAILS_ENV'] == 'development' || ENV['IDE_PROCESS_DISPATCHER'] +# timeout = 10000 +# end +# +# Rack::Timeout.timeout = timeout # seconds \ No newline at end of file diff --git a/config/routes.rb b/config/routes.rb index 0de6178e..b56dd437 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Commitchange::Application.routes.draw do +Rails.application.routes.draw do mount Houdini::API => '/api' if Rails.env == 'development' @@ -196,47 +196,47 @@ Commitchange::Application.routes.draw do :confirmations => 'users/confirmations' } devise_scope :user do - match '/sign_in' => 'users/sessions#new' - match '/signup' => 'devise/registrations#new' - post '/confirm' => 'users/confirmations#confirm' - match '/users/is_confirmed' => 'users/confirmations#is_confirmed' - match '/users/exists' => 'users/confirmations#exists' - post '/users/confirm_auth', action: :confirm_auth, controller: 'users/sessions' + match '/sign_in' => 'users/sessions#new', via: [:get, :post] + match '/signup' => 'devise/registrations#new', via: [:get, :post] + post '/confirm' => 'users/confirmations#confirm', via: [:get] + match '/users/is_confirmed' => 'users/confirmations#is_confirmed', via: [:get, :post] + match '/users/exists' => 'users/confirmations#exists', via: [:get] + post '/users/confirm_auth', action: :confirm_auth, controller: 'users/sessions', via: [:get, :post] end # Super admin - match '/admin' => 'super_admins#index', :as => 'admin' - match '/admin/search-nonprofits' => 'super_admins#search_nonprofits' - match '/admin/search-profiles' => 'super_admins#search_profiles' - match '/admin/search-fullcontact' => 'super_admins#search_fullcontact' - match '/admin/recurring-donations-without-cards' => 'super_admins#recurring_donations_without_cards' - match '/admin/export_supporters_with_rds' => 'super_admins#export_supporters_with_rds' - match '/admin/resend_user_confirmation' => 'super_admins#resend_user_confirmation' + match '/admin' => 'super_admins#index', :as => 'admin', via: [:get, :post] + match '/admin/search-nonprofits' => 'super_admins#search_nonprofits', via: [:get, :post] + match '/admin/search-profiles' => 'super_admins#search_profiles', via: [:get, :post] + match '/admin/search-fullcontact' => 'super_admins#search_fullcontact', via: [:get, :post] + match '/admin/recurring-donations-without-cards' => 'super_admins#recurring_donations_without_cards', via: [:get, :post] + match '/admin/export_supporters_with_rds' => 'super_admins#export_supporters_with_rds', via: [:get, :post] + match '/admin/resend_user_confirmation' => 'super_admins#resend_user_confirmation', via: [:get, :post] # Events - match '/events' => 'events#index' - match '/events/:event_slug' => 'events#show' + match '/events' => 'events#index', via: [:get] + match '/events/:event_slug' => 'events#show', via: [:get, :post] # Nonprofits - match ':state_code/:city/:name' => 'nonprofits#show', :as => :nonprofit_location - match ':state_code/:city/:name/donate' => 'nonprofits#donate', :as => :nonprofit_donation - match ':state_code/:city/:name/button' => 'nonprofits/button#guided' + match ':state_code/:city/:name' => 'nonprofits#show', :as => :nonprofit_location, via: [:get, :post] + match ':state_code/:city/:name/donate' => 'nonprofits#donate', :as => :nonprofit_donation, via: [:get, :post] + match ':state_code/:city/:name/button' => 'nonprofits/button#guided', via: [:get, :post] # Campaigns - match ':state_code/:city/:name/campaigns' => 'campaigns#index' - match ':state_code/:city/:name/campaigns/:campaign_slug' => 'campaigns#show', :as => :campaign_loc - match ':state_code/:city/:name/campaigns/:campaign_slug/supporters' => 'campaigns/supporters#index', :as => :campaign_loc - match '/peer-to-peer' => 'campaigns#peer_to_peer' + match ':state_code/:city/:name/campaigns' => 'campaigns#index', via: [:get, :post] + match ':state_code/:city/:name/campaigns/:campaign_slug' => 'campaigns#show', via: [:get, :post] + match ':state_code/:city/:name/campaigns/:campaign_slug/supporters' => 'campaigns/supporters#index', via: [:get, :post] + match '/peer-to-peer' => 'campaigns#peer_to_peer', via: [:get, :post] # Events - match ':state_code/:city/:name/events' => 'events#index' - match ':state_code/:city/:name/events/:event_slug' => 'events#show' - match ':state_code/:city/:name/events/:event_slug/stats' => 'events#stats' - match ':state_code/:city/:name/events/:event_slug/tickets' => 'tickets#index' + match ':state_code/:city/:name/events' => 'events#index', via: [:get, :post] + match ':state_code/:city/:name/events/:event_slug' => 'events#show', via: [:get, :post] + match ':state_code/:city/:name/events/:event_slug/stats' => 'events#stats', via: [:get, :post] + match ':state_code/:city/:name/events/:event_slug/tickets' => 'tickets#index', via: [:get, :post] # get '/events' => 'events#index' # Dashboard - match ':state_code/:city/:name/dashboard' => 'nonprofits#dashboard', as: :np_dashboard + match ':state_code/:city/:name/dashboard' => 'nonprofits#dashboard', as: :np_dashboard, via: [:get, :post] # Misc get '/pages/wp-plugin', to: redirect('/help/wordpress-plugin') #temporary, until WP plugin updated @@ -248,7 +248,7 @@ Commitchange::Application.routes.draw do get '/maps/specific-npo-supporters' => 'maps#specific_npo_supporters' # Mailchimp Landing - match '/mailchimp-landing' => 'nonprofits/nonprofit_keys#mailchimp_landing' + match '/mailchimp-landing' => 'nonprofits/nonprofit_keys#mailchimp_landing', via: [:get, :post] # Webhooks post '/webhooks/stripe_subscription_payment' => 'webhooks#subscription_payment' diff --git a/config/secrets.yml b/config/secrets.yml new file mode 100644 index 00000000..499f969f --- /dev/null +++ b/config/secrets.yml @@ -0,0 +1,22 @@ +# Be sure to restart your server when you modify this file. + +# Your secret key is used for verifying the integrity of signed cookies. +# If you change this key, all old signed cookies will become invalid! + +# Make sure the secret is at least 30 characters and all random, +# no regular words or you'll be exposed to dictionary attacks. +# You can use `rake secret` to generate a secure secret key. + +# Make sure the secrets in this file are kept private +# if you're sharing your code publicly. + +development: + secret_key_base: 2d40128da31b5d45c2db72181b23eb1d2ca216517e033921a07febee5350179627b4ea75f898b71e2641df86ceed0e45a8a052731a1ce8420fe07d0f3840688d + +test: + secret_key_base: e0cdefb8725ea588ef3d2786bbfe5b79c2394f9c2203662c246a5fbdf20aeb3ecf4ae81f27d276139c661ad7091fc0060621edba2837d55b0fe7726a2de359c6 + +# Do not keep production secrets in the repository, +# instead read values from the environment. +production: + secret_key_base: <%= ENV["SECRET_KEY_BASE"] %> diff --git a/db/structure.sql b/db/structure.sql index 0587bd47..416ee5e2 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -3,7 +3,7 @@ -- -- Dumped from database version 9.6.5 --- Dumped by pg_dump version 9.6.11 +-- Dumped by pg_dump version 9.6.10 SET statement_timeout = 0; SET lock_timeout = 0; diff --git a/lib/create/create_campaign.rb b/lib/create/create_campaign.rb index b9b7dd03..7233d661 100644 --- a/lib/create/create_campaign.rb +++ b/lib/create/create_campaign.rb @@ -2,4 +2,32 @@ module CreateCampaign CAMPAIGN_NAME_LENGTH_LIMIT = 60 + + # @return [Object] a json object for historical purposes + def self.create(params, nonprofit) + Time.use_zone(nonprofit.timezone || 'UTC') do + params[:campaign][:end_datetime] = Chronic.parse(params[:campaign][:end_datetime]) if params[:campaign][:end_datetime].present? + end + + if !params[:campaign][:parent_campaign_id] + campaign = nonprofit.campaigns.create params[:campaign] + + #do notifications + user = campaign.profile.user + Role.create(name: :campaign_editor, user_id: user.id, host: self) + CampaignMailer.delay.creation_followup(self) + NonprofitAdminMailer.delay.supporter_fundraiser(self) unless QueryRoles.is_nonprofit_user?(user.id, self.nonprofit_id) + + return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? + return campaign.as_json + #json_saved campaign, 'Campaign created! Well done.' + else + profile_id = params[:campaign][:profile_id] + Profile.find(profile_id).update_attributes params[:profile] + return CreatePeerToPeerCampaign.create(params[:campaign], profile_id) + end + end + + + end \ No newline at end of file diff --git a/lib/email.rb b/lib/email.rb index 4b34cb5d..d255f6e6 100644 --- a/lib/email.rb +++ b/lib/email.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Email - Regex ||= /^[^ ]+@[^ ]+\.[^ ]+/i + Regex ||= /\A[^ ]+@[^ ]+\.[^ ]+/i #PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$' end diff --git a/lib/generators/api/resource/templates/spec.rb.erb b/lib/generators/api/resource/templates/spec.rb.erb index c47d73e6..66d129b1 100644 --- a/lib/generators/api/resource/templates/spec.rb.erb +++ b/lib/generators/api/resource/templates/spec.rb.erb @@ -2,7 +2,7 @@ require 'rails_helper' describe Houdini::V1::<%= name.camelcase %>, :type => :request do - describe :get do + describe 'get', :get do end end \ No newline at end of file diff --git a/spec/api/houdini/nonprofit_spec.rb b/spec/api/houdini/nonprofit_spec.rb index daddc8ba..75ab3a7d 100644 --- a/spec/api/houdini/nonprofit_spec.rb +++ b/spec/api/houdini/nonprofit_spec.rb @@ -1,18 +1,18 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' -describe Houdini::V1::Nonprofit, :type => :controller do - describe :get do +describe Houdini::V1::Nonprofit, :type => :request do + describe 'get' do end - describe :post do - around {|e| + describe 'post' do + around(:each) do |example| @old_bp =Settings.default_bp - e.run + example.run Settings.default_bp = @old_bp - } + end def expect_validation_errors(actual, input) expected_errors = input.with_indifferent_access[:errors] expect(actual["errors"]).to match_array expected_errors @@ -47,7 +47,7 @@ describe Houdini::V1::Nonprofit, :type => :controller do }.with_indifferent_access } describe 'authorization' do - around {|e| + around(:each) {|e| Rails.configuration.action_controller.allow_forgery_protection = true e.run Rails.configuration.action_controller.allow_forgery_protection = false diff --git a/spec/controllers/campaign_gift_options_spec.rb b/spec/controllers/campaign_gift_options_spec.rb index d2878734..84d20f4f 100644 --- a/spec/controllers/campaign_gift_options_spec.rb +++ b/spec/controllers/campaign_gift_options_spec.rb @@ -11,14 +11,14 @@ describe CampaignGiftOptionsController, :type => :controller do end describe 'update' do - include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" end describe 'destroy' do - include_context :open_to_campaign_editor, :delete, :destroy, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :delete, :destroy, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" end describe 'update_order' do - include_context :open_to_campaign_editor, :put, :update_order, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :put, :update_order, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" end end @@ -28,7 +28,7 @@ describe CampaignGiftOptionsController, :type => :controller do end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" end end end diff --git a/spec/controllers/campaigns_spec.rb b/spec/controllers/campaigns_spec.rb index a7cf07f8..0841b4b6 100644 --- a/spec/controllers/campaigns_spec.rb +++ b/spec/controllers/campaigns_spec.rb @@ -15,15 +15,15 @@ describe CampaignsController, :type => :controller do end describe 'duplicate' do - include_context :open_to_confirmed_users, :post, :duplicate, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_confirmed_users, :post, :duplicate, nonprofit_id: :__our_np, id: :__our_campaign end describe 'update' do - include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_campaign end describe 'soft_delete' do - include_context :open_to_campaign_editor, :delete, :soft_delete, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :delete, :soft_delete, nonprofit_id: :__our_np, id: :__our_campaign end end @@ -33,33 +33,34 @@ describe CampaignsController, :type => :controller do end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_campaign end describe 'activities' do - include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_campaign end describe 'metrics' do - include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_campaign end describe 'timeline' do - include_context :open_to_all, :get, :timeline, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :timeline, nonprofit_id: :__our_np, id: :__our_campaign end describe 'totals' do - include_context :open_to_all, :get, :totals, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :totals, nonprofit_id: :__our_np, id: :__our_campaign end describe 'peer_to_peer' do include_context :open_to_all, :get, :peer_to_peer, nonprofit_id: :__our_np end + end + end - - - - + describe 'routes' do + it "routes campaigns#index" do + expect(:get => "/nonprofits/5/campaigns/4").to(route_to(:controller => "campaigns", :action => "show", nonprofit_id: "5", id: "4")) end end end \ No newline at end of file diff --git a/spec/controllers/event_discounts_spec.rb b/spec/controllers/event_discounts_spec.rb index 37cfcaba..4a723b8f 100644 --- a/spec/controllers/event_discounts_spec.rb +++ b/spec/controllers/event_discounts_spec.rb @@ -11,11 +11,11 @@ describe EventDiscountsController, :type => :controller do end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end @@ -23,7 +23,7 @@ describe EventDiscountsController, :type => :controller do describe 'open to all' do describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event, id: "2" end end end diff --git a/spec/controllers/events_spec.rb b/spec/controllers/events_spec.rb index 7bef9f29..09e4f6b0 100644 --- a/spec/controllers/events_spec.rb +++ b/spec/controllers/events_spec.rb @@ -6,19 +6,19 @@ describe EventsController, :type => :controller do describe 'authorization' do include_context :shared_user_context describe 'create' do - include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, id: :__our_event end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_event end describe 'duplicate' do - include_context :open_to_event_editor, :post, :duplicate, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :post, :duplicate, nonprofit_id: :__our_np, id: :__our_event end describe 'soft_delete' do - include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, id: :__our_event end describe 'stats' do - include_context :open_to_event_editor, :get, :stats, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :get, :stats, nonprofit_id: :__our_np, id: :__our_event end describe 'name_and_id' do @@ -36,14 +36,14 @@ describe EventsController, :type => :controller do end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_event end describe 'activities' do - include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np + include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_event end describe 'metrics' do - include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np + include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_event end diff --git a/spec/controllers/nonprofits/activities_spec.rb b/spec/controllers/nonprofits/activities_spec.rb index 3b59f248..34800fcd 100644 --- a/spec/controllers/nonprofits/activities_spec.rb +++ b/spec/controllers/nonprofits/activities_spec.rb @@ -7,7 +7,7 @@ describe Nonprofits::ActivitiesController, :type => :controller do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'get' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :index, id: :__our_np end end end diff --git a/spec/controllers/nonprofits/custom_field_masters_spec.rb b/spec/controllers/nonprofits/custom_field_masters_spec.rb index dcdb1f63..a7bf7953 100644 --- a/spec/controllers/nonprofits/custom_field_masters_spec.rb +++ b/spec/controllers/nonprofits/custom_field_masters_spec.rb @@ -14,7 +14,7 @@ describe Nonprofits::CustomFieldMastersController, :type => :controller do end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end end \ No newline at end of file diff --git a/spec/controllers/nonprofits/custom_fields_joins_spec.rb b/spec/controllers/nonprofits/custom_fields_joins_spec.rb index c6c02cc3..5a8fd8c1 100644 --- a/spec/controllers/nonprofits/custom_fields_joins_spec.rb +++ b/spec/controllers/nonprofits/custom_fields_joins_spec.rb @@ -10,11 +10,11 @@ describe Nonprofits::CustomFieldJoinsController, :type => :controller do end describe 'modify' do - include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np + include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: "1" end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: "1" end end end \ No newline at end of file diff --git a/spec/controllers/nonprofits/donations_spec.rb b/spec/controllers/nonprofits/donations_spec.rb index 24e45032..f2cdb4f7 100644 --- a/spec/controllers/nonprofits/donations_spec.rb +++ b/spec/controllers/nonprofits/donations_spec.rb @@ -9,14 +9,14 @@ describe Nonprofits::DonationsController, :type => :controller do describe 'rejects unauthenticated users' do describe 'index' do include_context :shared_user_context - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, id: "1" end describe 'update' do include_context :shared_user_context - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: "1" end @@ -27,7 +27,7 @@ describe Nonprofits::DonationsController, :type => :controller do end describe 'follow up' do - include_context :open_to_all, :put, :followup, nonprofit_id: :__our_np + include_context :open_to_all, :put, :followup, nonprofit_id: :__our_np, id: "1" end end end diff --git a/spec/controllers/nonprofits/payments_spec.rb b/spec/controllers/nonprofits/payments_spec.rb index a23537d8..7750b6df 100644 --- a/spec/controllers/nonprofits/payments_spec.rb +++ b/spec/controllers/nonprofits/payments_spec.rb @@ -14,15 +14,15 @@ describe Nonprofits::PaymentsController, :type => :controller do end describe 'show payments' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1' end describe 'update' do - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: '1' end describe 'destroy payment' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end end \ No newline at end of file diff --git a/spec/controllers/nonprofits/payouts_spec.rb b/spec/controllers/nonprofits/payouts_spec.rb index 677b4625..82a5fc96 100644 --- a/spec/controllers/nonprofits/payouts_spec.rb +++ b/spec/controllers/nonprofits/payouts_spec.rb @@ -14,7 +14,7 @@ describe Nonprofits::PayoutsController, :type => :controller do end describe 'show' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1' end diff --git a/spec/controllers/nonprofits/recurring_donations_spec.rb b/spec/controllers/nonprofits/recurring_donations_spec.rb index a218a1f4..4ad14e70 100644 --- a/spec/controllers/nonprofits/recurring_donations_spec.rb +++ b/spec/controllers/nonprofits/recurring_donations_spec.rb @@ -14,15 +14,15 @@ describe Nonprofits::RecurringDonationsController, :type => :controller do end describe 'show' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1' end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end describe 'update' do - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: '1' end diff --git a/spec/controllers/nonprofits/supporters_spec.rb b/spec/controllers/nonprofits/supporters_spec.rb index 0c3e473c..ac544e24 100644 --- a/spec/controllers/nonprofits/supporters_spec.rb +++ b/spec/controllers/nonprofits/supporters_spec.rb @@ -14,23 +14,23 @@ describe Nonprofits::SupportersController, :type => :controller do end describe 'show' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1' end describe 'email_address' do - include_context :open_to_np_associate, :get, :email_address, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :email_address, nonprofit_id: :__our_np, id: '1' end describe 'full_contact' do - include_context :open_to_np_associate, :get, :full_contact, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :full_contact, nonprofit_id: :__our_np, id: '1' end describe 'info_card' do - include_context :open_to_np_associate, :get, :info_card, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :info_card, nonprofit_id: :__our_np, id: '1' end describe 'update' do - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: '1' end describe 'bulk_delete' do diff --git a/spec/controllers/nonprofits/tag_joins_spec.rb b/spec/controllers/nonprofits/tag_joins_spec.rb index 927fef46..1f996909 100644 --- a/spec/controllers/nonprofits/tag_joins_spec.rb +++ b/spec/controllers/nonprofits/tag_joins_spec.rb @@ -10,11 +10,11 @@ describe Nonprofits::TagJoinsController, :type => :controller do end describe 'modify' do - include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np + include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: '1' end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end diff --git a/spec/controllers/nonprofits/tag_masters_spec.rb b/spec/controllers/nonprofits/tag_masters_spec.rb index 5ad258a6..999f4072 100644 --- a/spec/controllers/nonprofits/tag_masters_spec.rb +++ b/spec/controllers/nonprofits/tag_masters_spec.rb @@ -15,7 +15,7 @@ describe Nonprofits::TagMastersController, :type => :controller do end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end end diff --git a/spec/controllers/nonprofits_spec.rb b/spec/controllers/nonprofits_spec.rb index ec4099c3..67b6fe6f 100644 --- a/spec/controllers/nonprofits_spec.rb +++ b/spec/controllers/nonprofits_spec.rb @@ -7,54 +7,47 @@ describe NonprofitsController, :type => :controller do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'update' do - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :update, id: :__our_np end describe 'dashboard' do - include_context :open_to_np_associate, :get, :dashboard, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :dashboard, id: :__our_np end describe 'dashboard_metrics' do - include_context :open_to_np_associate, :get, :dashboard_metrics, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :dashboard_metrics, id: :__our_np end describe 'verify_identity' do - include_context :open_to_np_associate, :put, :verify_identity, nonprofit_id: :__our_np + include_context :open_to_np_associate, :put, :verify_identity, id: :__our_np end describe 'recurring_donation_stats' do - include_context :open_to_np_associate, :get, :recurring_donation_stats, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :recurring_donation_stats, id: :__our_np end describe 'profile_todos' do - include_context :open_to_np_associate, :get, :profile_todos, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :profile_todos, id: :__our_np end describe 'dashboard_todos' do - include_context :open_to_np_associate, :get, :dashboard_todos, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :dashboard_todos, id: :__our_np end describe 'payment_history' do - include_context :open_to_np_associate, :get, :payment_history, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :payment_history, id: :__our_np end describe 'destroy' do - include_context :open_to_super_admin, :delete, :destroy + include_context :open_to_super_admin, :delete, :destroy, id: :__our_np end - - - - - - - end describe 'open to all' do describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np + include_context :open_to_all, :get, :show, id: :__our_np end describe 'create' do @@ -62,19 +55,19 @@ describe NonprofitsController, :type => :controller do end describe 'btn' do - include_context :open_to_all, :get, :btn, nonprofit_id: :__our_np + include_context :open_to_all, :get, :btn, id: :__our_np end describe 'supporter_form' do - include_context :open_to_all, :get, :supporter_form, nonprofit_id: :__our_np + include_context :open_to_all, :get, :supporter_form, id: :__our_np end describe 'custom_supporter' do - include_context :open_to_all, :post, :custom_supporter, nonprofit_id: :__our_np + include_context :open_to_all, :post, :custom_supporter, id: :__our_np end describe 'donate' do - include_context :open_to_all, :get, :donate, nonprofit_id: :__our_np + include_context :open_to_all, :get, :donate, id: :__our_np end describe 'search' do diff --git a/spec/controllers/profiles_spec.rb b/spec/controllers/profiles_spec.rb index 181fa3a1..764f08b5 100644 --- a/spec/controllers/profiles_spec.rb +++ b/spec/controllers/profiles_spec.rb @@ -21,7 +21,7 @@ describe ProfilesController, :type => :controller do describe 'open to all' do describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np + include_context :open_to_all, :get, :show, id: :__our_np end end end diff --git a/spec/controllers/recurring_donations_spec.rb b/spec/controllers/recurring_donations_spec.rb index 5876b6e0..122a04cc 100644 --- a/spec/controllers/recurring_donations_spec.rb +++ b/spec/controllers/recurring_donations_spec.rb @@ -7,22 +7,20 @@ describe RecurringDonationsController, :type => :controller do include_context :shared_user_context describe 'open to all (note: edit token is checked inside methods)' do describe 'edit' do - include_context :open_to_all, :get, :edit, nonprofit_id: :__our_np + include_context :open_to_all, :get, :edit, nonprofit_id: :__our_np, id: '1' end describe 'destroy' do - include_context :open_to_all, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_all, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end describe 'update' do - include_context :open_to_all, :put, :update, nonprofit_id: :__our_np + include_context :open_to_all, :put, :update, nonprofit_id: :__our_np, id: '1' end describe 'update_amount' do - include_context :open_to_all, :put, :update_amount, nonprofit_id: :__our_np + include_context :open_to_all, :put, :update_amount, nonprofit_id: :__our_np, id: '1' end - - end end end \ No newline at end of file diff --git a/spec/controllers/roles_spec.rb b/spec/controllers/roles_spec.rb index cfc87a30..6b746d62 100644 --- a/spec/controllers/roles_spec.rb +++ b/spec/controllers/roles_spec.rb @@ -11,7 +11,7 @@ describe RolesController, :type => :controller do end describe 'destroy' do - include_context :open_to_np_admin, :delete, :destroy, nonprofit_id: :__our_np + include_context :open_to_np_admin, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end diff --git a/spec/controllers/ticket_levels_spec.rb b/spec/controllers/ticket_levels_spec.rb index 53687df7..d9b5cd6f 100644 --- a/spec/controllers/ticket_levels_spec.rb +++ b/spec/controllers/ticket_levels_spec.rb @@ -7,27 +7,27 @@ describe TicketLevelsController, :type => :controller do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do - include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" end describe 'update_order' do include_context :open_to_event_editor, :put, :update_order, nonprofit_id: :__our_np, event_id: :__our_event end describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" end end describe 'open to all' do describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event end end end diff --git a/spec/lib/create/create_campaign_spec.rb b/spec/lib/create/create_campaign_spec.rb new file mode 100644 index 00000000..a44170a5 --- /dev/null +++ b/spec/lib/create/create_campaign_spec.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +describe CreateCampaign do + it 'is untested' do + pending 'add tests here' + end +end \ No newline at end of file diff --git a/spec/lib/query/query_donations_spec.rb b/spec/lib/query/query_donations_spec.rb index 3741b275..34088427 100644 --- a/spec/lib/query/query_donations_spec.rb +++ b/spec/lib/query/query_donations_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' describe QueryDonations do - describe :campaign_export do + describe 'campaign_export' do let(:nonprofit) {force_create(:nonprofit)} let(:supporter) {force_create(:supporter)} From 881e42cc0b7c9c2559e463534cc9d331f6dd93db Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 16 Jan 2019 14:33:21 -0600 Subject: [PATCH 012/440] Correct rubocop warning --- spec/factories/billing_plans.rb | 4 ++-- spec/factories/campaign_gift_options.rb | 2 +- spec/factories/cards.rb | 8 ++++---- spec/factories/custom_field_joins.rb | 10 +++++----- spec/factories/custom_field_masters.rb | 4 ++-- spec/factories/events.rb | 14 +++++++------- spec/factories/nonprofits.rb | 12 ++++++------ spec/factories/payment_imports.rb | 4 ++-- spec/factories/supporters.rb | 2 +- spec/factories/tag_joins.rb | 8 ++++---- spec/factories/users.rb | 2 +- 11 files changed, 35 insertions(+), 35 deletions(-) diff --git a/spec/factories/billing_plans.rb b/spec/factories/billing_plans.rb index 05ffea0c..a86c08ac 100644 --- a/spec/factories/billing_plans.rb +++ b/spec/factories/billing_plans.rb @@ -1,8 +1,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :billing_plan do - amount 0 - name 'Default Plan' + amount { 0 } + name { 'Default Plan' } trait :default do end diff --git a/spec/factories/campaign_gift_options.rb b/spec/factories/campaign_gift_options.rb index 31efecee..48f8e08b 100644 --- a/spec/factories/campaign_gift_options.rb +++ b/spec/factories/campaign_gift_options.rb @@ -3,6 +3,6 @@ FactoryBot.define do factory :campaign_gift_option do sequence(:name) {|i| "name_#{i}"} campaign - amount_one_time 200 + amount_one_time { 200 } end end diff --git a/spec/factories/cards.rb b/spec/factories/cards.rb index a26d490b..5dd70c97 100644 --- a/spec/factories/cards.rb +++ b/spec/factories/cards.rb @@ -3,14 +3,14 @@ FactoryBot.define do factory :card do factory :active_card_1 do - name 'card 1' + name { 'card 1' } end factory :active_card_2 do - name 'card 1' + name { 'card 1' } end factory :inactive_card do - name 'card 1' - inactive true + name { 'card 1' } + inactive { true } end diff --git a/spec/factories/custom_field_joins.rb b/spec/factories/custom_field_joins.rb index cbaab8f2..ae2c56c1 100644 --- a/spec/factories/custom_field_joins.rb +++ b/spec/factories/custom_field_joins.rb @@ -1,11 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :custom_field_join do - custom_field_master_id 1 - supporter_id 4 - created_at DateTime.now - updated_at DateTime.now - value 'value' + custom_field_master_id { 1 } + supporter_id { 4 } + created_at { DateTime.now } + updated_at { DateTime.now } + value { 'value' } trait :value_from_id do after(:create) do |cfj| diff --git a/spec/factories/custom_field_masters.rb b/spec/factories/custom_field_masters.rb index b8b45d84..cc1848ef 100644 --- a/spec/factories/custom_field_masters.rb +++ b/spec/factories/custom_field_masters.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :custom_field_master do - nonprofit "" - name "MyString" + nonprofit { "" } + name { "MyString" } end end diff --git a/spec/factories/events.rb b/spec/factories/events.rb index b5307176..50557398 100644 --- a/spec/factories/events.rb +++ b/spec/factories/events.rb @@ -1,13 +1,13 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :event do - name "The event of Wonders" - start_datetime DateTime.new(2025, 5, 11, 4,5,6) - end_datetime DateTime.new(2025, 5, 11, 5,1,7) - address "100 N Appleton St" - city "Appleton" - state_code "WI" - slug "event-of-wonders" + name { "The event of Wonders" } + start_datetime { DateTime.new(2025, 5, 11, 4,5,6) } + end_datetime { DateTime.new(2025, 5, 11, 5,1,7) } + address { "100 N Appleton St" } + city { "Appleton" } + state_code { "WI" } + slug { "event-of-wonders" } nonprofit profile end diff --git a/spec/factories/nonprofits.rb b/spec/factories/nonprofits.rb index c555f9a0..d32be4de 100644 --- a/spec/factories/nonprofits.rb +++ b/spec/factories/nonprofits.rb @@ -1,12 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :nonprofit do - name "spec_nonprofit_full" - city 'Albuquerque' - state_code 'NM' - zip_code 55555 - email "example@email.com" - slug 'sluggy-sluggo' + name { "spec_nonprofit_full" } + city { 'Albuquerque' } + state_code { 'NM' } + zip_code { 55555 } + email { "example@email.com" } + slug { 'sluggy-sluggo' } factory :nonprofit_with_cards do diff --git a/spec/factories/payment_imports.rb b/spec/factories/payment_imports.rb index 6dd38917..b42c0cce 100644 --- a/spec/factories/payment_imports.rb +++ b/spec/factories/payment_imports.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :payment_import do - user "" - nonprofit "" + user { "" } + nonprofit { "" } end end diff --git a/spec/factories/supporters.rb b/spec/factories/supporters.rb index 7a7c665e..2b119a5c 100644 --- a/spec/factories/supporters.rb +++ b/spec/factories/supporters.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :supporter do - name "Fake Supporter Name" + name { "Fake Supporter Name" } nonprofit trait :has_a_card do diff --git a/spec/factories/tag_joins.rb b/spec/factories/tag_joins.rb index 626dcd9e..9bee566a 100644 --- a/spec/factories/tag_joins.rb +++ b/spec/factories/tag_joins.rb @@ -1,9 +1,9 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later FactoryBot.define do factory :tag_join do - tag_master_id 1 - supporter_id 4 - created_at DateTime.now - updated_at DateTime.now + tag_master_id { 1 } + supporter_id { 4 } + created_at { DateTime.now } + updated_at { DateTime.now } end end diff --git a/spec/factories/users.rb b/spec/factories/users.rb index c3f28efc..2ba069e4 100644 --- a/spec/factories/users.rb +++ b/spec/factories/users.rb @@ -2,6 +2,6 @@ FactoryBot.define do factory :user do sequence(:email) {|i| "user#{i}@example.string.com"} - password "whocares" + password { "whocares" } end end From a5903d9daf50d8961d18b2d60dbddb4873c5ea7f Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 16 Jan 2019 14:38:59 -0600 Subject: [PATCH 013/440] Correct blocklist name WIP --- config/environments/test.rb | 4 ++-- config/initializers/block_ips.rb | 2 +- spec/controllers/events_spec.rb | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index 48544a21..728e452a 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,11 +1,11 @@ -Rails.application.configure do +Commitchange::Application.configure do # Settings specified here will take precedence over those in config/application.rb. # The test environment is used exclusively to run your application's # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = true + config.cache_classes = false # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that diff --git a/config/initializers/block_ips.rb b/config/initializers/block_ips.rb index 5e547e78..ed9f52f0 100644 --- a/config/initializers/block_ips.rb +++ b/config/initializers/block_ips.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Rack::Attack.blacklist('block charge abusers') do |req| +Rack::Attack.blocklist('block charge abusers') do |req| ['54.159.242.229', '54.161.246.233', '54.211.94.199' diff --git a/spec/controllers/events_spec.rb b/spec/controllers/events_spec.rb index 09e4f6b0..5d832eb6 100644 --- a/spec/controllers/events_spec.rb +++ b/spec/controllers/events_spec.rb @@ -15,7 +15,7 @@ describe EventsController, :type => :controller do include_context :open_to_event_editor, :post, :duplicate, nonprofit_id: :__our_np, id: :__our_event end describe 'soft_delete' do - include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, event_id: :__our_event end describe 'stats' do include_context :open_to_event_editor, :get, :stats, nonprofit_id: :__our_np, id: :__our_event From 30accb5e7922579a23eb8742fa96b1e6b4723f1a Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 25 Jan 2019 16:22:05 -0600 Subject: [PATCH 014/440] Add solargraph --- Gemfile | 1 + Gemfile.lock | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/Gemfile b/Gemfile index 647c0c7f..cd0d63c9 100755 --- a/Gemfile +++ b/Gemfile @@ -116,6 +116,7 @@ group :development, :ci, :test do gem 'factory_bot_rails' gem 'action_mailer_matchers' gem 'simplecov', '~> 0.16.1', require: false + gem 'solargraph' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index aa61483f..947a66ab 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,6 +67,7 @@ GEM amq-protocol (2.2.0) andand (1.3.3) arel (5.0.1.20140414130214) + ast (2.4.0) aws-eventstream (1.0.1) aws-partitions (1.110.0) aws-sdk (1.67.0) @@ -96,6 +97,7 @@ GEM descendants_tracker (~> 0.0.4) ice_nine (~> 0.11.0) thread_safe (~> 0.3, >= 0.3.1) + backport (0.3.0) bcrypt (3.1.12) binding_of_caller (0.8.0) debug_inspector (>= 0.0.1) @@ -239,6 +241,7 @@ GEM hashie (3.6.0) heroku-deflater (0.6.3) rack (>= 1.4.5) + htmlentities (4.3.4) http-cookie (1.0.3) domain_name (~> 0.5) httparty (0.16.2) @@ -249,9 +252,11 @@ GEM i18n (>= 0.6.6, < 2) i18n_data (0.8.0) ice_nine (0.11.2) + jaro_winkler (1.5.2) jmespath (1.4.0) json (1.8.6) kdtree (0.4) + kramdown (1.17.0) lograge (0.10.0) actionpack (>= 4) activesupport (>= 4) @@ -284,6 +289,8 @@ GEM mini_portile2 (~> 2.3.0) orm_adapter (0.5.0) parallel (1.12.1) + parser (2.6.0.0) + ast (~> 2.4.0) pg (0.21.0) polyglot (0.3.5) power_assert (1.1.1) @@ -331,6 +338,7 @@ GEM rack-ssl (~> 1.3.2) rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) + rainbow (3.0.0) rake (12.3.2) request_store (1.4.1) rack (>= 1.4) @@ -341,6 +349,8 @@ GEM http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) netrc (~> 0.8) + reverse_markdown (1.1.0) + nokogiri roadie (3.4.0) css_parser (~> 1.4) nokogiri (~> 1.5) @@ -368,9 +378,18 @@ GEM rspec-mocks (~> 3.8.0) rspec-support (~> 3.8.0) rspec-support (3.8.0) + rubocop (0.63.1) + jaro_winkler (~> 1.5.1) + parallel (~> 1.10) + parser (>= 2.5, != 2.5.1.1) + powerpack (~> 0.1) + rainbow (>= 2.2.2, < 4.0) + ruby-progressbar (~> 1.7) + unicode-display_width (~> 1.4.0) ruby-debug-ide (0.6.1) rake (>= 0.8.1) ruby-prof (0.15.9) + ruby-progressbar (1.10.0) safe_yaml (1.0.4) sass (3.2.19) sass-rails (5.0.7) @@ -385,6 +404,17 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) sixarm_ruby_unaccent (1.2.0) + solargraph (0.31.1) + backport (~> 0.3) + htmlentities (~> 4.3, >= 4.3.4) + jaro_winkler (~> 1.5) + kramdown (~> 1.16) + parser (~> 2.3) + reverse_markdown (~> 1.0, >= 1.0.5) + rubocop (~> 0.52) + thor (~> 0.19, >= 0.19.4) + tilt (~> 2.0) + yard (~> 0.9) sprockets (3.7.2) concurrent-ruby (~> 1.0) rack (> 1, < 3) @@ -410,6 +440,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.5) + unicode-display_width (1.4.1) unicode_utils (1.4.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -423,6 +454,7 @@ GEM crack (>= 0.3.2) hashdiff xml-simple (1.1.5) + yard (0.9.18) PLATFORMS ruby @@ -494,6 +526,7 @@ DEPENDENCIES sass (= 3.2.19) sass-rails simplecov (~> 0.16.1) + solargraph sprockets stripe stripe-ruby-mock (~> 2.4.1)! From 3d27e97feea4a8aff664e8657de4ae0cdcdc065d Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 13:27:19 -0600 Subject: [PATCH 015/440] Fix bug where checking payment providers were validated improperly in rails 4.1 --- app/models/donation.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/donation.rb b/app/models/donation.rb index d014f737..43f995c9 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -25,7 +25,7 @@ class Donation < ActiveRecord::Base validates :supporter, presence: true validates :nonprofit, presence: true validates_associated :charges - validates :payment_provider, inclusion: { in: %(credit_card sepa) }, allow_blank: true + validates :payment_provider, inclusion: { in: ["credit_card", "sepa"]}, allow_blank: true has_many :charges has_many :campaign_gifts, dependent: :destroy From 3f23b3df8e9cd67ca2446d76e4c42c621855112e Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 14:07:34 -0600 Subject: [PATCH 016/440] Turn delivery errors off in test environment --- config/environments/test.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 728e452a..fb680fe0 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -30,6 +30,7 @@ Commitchange::Application.configure do # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test + config.action_mailer.raise_delivery_errors = false # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr From 5f7b284c4f08252136e3fe8c34ff34d0a33a8a1f Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 14:12:43 -0600 Subject: [PATCH 017/440] Fix bug where delete_card_for_ticket spec was failing --- spec/controllers/tickets_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/tickets_spec.rb b/spec/controllers/tickets_spec.rb index 6bda138d..b6b05885 100644 --- a/spec/controllers/tickets_spec.rb +++ b/spec/controllers/tickets_spec.rb @@ -19,7 +19,7 @@ describe TicketsController, :type => :controller do end describe 'delete_card_for_ticket' do - include_context :open_to_np_associate, :post, :delete_card_for_ticket, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_np_associate, :post, :delete_card_for_ticket, nonprofit_id: :__our_np, event_id: :__our_event, id: 11111 end From b4ebaa81024e843b870ff7fc94e5f75359700b0b Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 14:17:10 -0600 Subject: [PATCH 018/440] Fix routing bugs in TicketsController spec --- spec/controllers/tickets_spec.rb | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/spec/controllers/tickets_spec.rb b/spec/controllers/tickets_spec.rb index b6b05885..23faaa04 100644 --- a/spec/controllers/tickets_spec.rb +++ b/spec/controllers/tickets_spec.rb @@ -6,32 +6,31 @@ describe TicketsController, :type => :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do - describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event - end + describe 'index' do - include_context :open_to_event_editor, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event end + describe 'update' do + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 + end describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 end describe 'delete_card_for_ticket' do include_context :open_to_np_associate, :post, :delete_card_for_ticket, nonprofit_id: :__our_np, event_id: :__our_event, id: 11111 end - - end describe 'open to all' do describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event end describe 'add_note' do - include_context :open_to_all, :put, :add_note, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_all, :put, :add_note, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 end end From cb1c5f2f8c54806e15229d072509e22d5daabebd Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 14:28:15 -0600 Subject: [PATCH 019/440] Fix route bugs for nonprofits_controller --- config/routes.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index b56dd437..ca533d11 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -121,11 +121,11 @@ Rails.application.routes.draw do resources(:nonprofits, {only: [:show, :create, :update, :destroy]}) do post(:onboard, {on: :collection}) - get(:profile_todos) - get(:recurring_donation_stats) + get(:profile_todos, {on: :member}) + get(:recurring_donation_stats, {on: :member}) get(:search, {on: :collection}) - get(:dashboard_todos) - put(:verify_identity) + get(:dashboard_todos, {on: :member}) + put(:verify_identity, {on: :member}) resources(:roles, {only: [:create, :destroy]}) From 568835c9bfa9ee3ff46415e4aba6c8f417977586 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 14:53:50 -0600 Subject: [PATCH 020/440] Fix a numb of invalid routes in Rails 4 --- config/routes.rb | 6 +++--- spec/controllers/nonprofits/activities_spec.rb | 2 +- spec/controllers/nonprofits/custom_fields_joins_spec.rb | 6 +++--- spec/controllers/nonprofits/tag_joins_spec.rb | 4 ++-- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index ca533d11..b9265366 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -90,9 +90,9 @@ Rails.application.routes.draw do put :bulk_delete, on: :collection post :merge, on: :collection get :merge_data, on: :collection - get :info_card - get :email_address - get :full_contact + get :info_card, on: :member + get :email_address, on: :member + get :full_contact, on: :member get :index_metrics, on: :collection end diff --git a/spec/controllers/nonprofits/activities_spec.rb b/spec/controllers/nonprofits/activities_spec.rb index 34800fcd..ec3dd383 100644 --- a/spec/controllers/nonprofits/activities_spec.rb +++ b/spec/controllers/nonprofits/activities_spec.rb @@ -7,7 +7,7 @@ describe Nonprofits::ActivitiesController, :type => :controller do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'get' do - include_context :open_to_np_associate, :get, :index, id: :__our_np + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, supporter_id: 1111 end end end diff --git a/spec/controllers/nonprofits/custom_fields_joins_spec.rb b/spec/controllers/nonprofits/custom_fields_joins_spec.rb index 5a8fd8c1..ab3a4e2d 100644 --- a/spec/controllers/nonprofits/custom_fields_joins_spec.rb +++ b/spec/controllers/nonprofits/custom_fields_joins_spec.rb @@ -5,8 +5,8 @@ require 'controllers/support/shared_user_context' describe Nonprofits::CustomFieldJoinsController, :type => :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do - describe 'index ' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + describe 'index' do + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, supporter_id: 1 end describe 'modify' do @@ -14,7 +14,7 @@ describe Nonprofits::CustomFieldJoinsController, :type => :controller do end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: "1" + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: "1", supporter_id: 1 end end end \ No newline at end of file diff --git a/spec/controllers/nonprofits/tag_joins_spec.rb b/spec/controllers/nonprofits/tag_joins_spec.rb index 1f996909..a9ff6327 100644 --- a/spec/controllers/nonprofits/tag_joins_spec.rb +++ b/spec/controllers/nonprofits/tag_joins_spec.rb @@ -6,7 +6,7 @@ describe Nonprofits::TagJoinsController, :type => :controller do describe 'authorization' do include_context :shared_user_context describe 'index' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, supporter_id: 1 end describe 'modify' do @@ -14,7 +14,7 @@ describe Nonprofits::TagJoinsController, :type => :controller do end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1', supporter_id: 2 end From 438ecf16c8b8002cc9b369c28a3bb3888626a054 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 15:02:42 -0600 Subject: [PATCH 021/440] Correct spec for campaign_gift_options --- spec/controllers/campaigns/campaign_gift_options_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/campaigns/campaign_gift_options_spec.rb b/spec/controllers/campaigns/campaign_gift_options_spec.rb index 9627d7fc..58221ca4 100644 --- a/spec/controllers/campaigns/campaign_gift_options_spec.rb +++ b/spec/controllers/campaigns/campaign_gift_options_spec.rb @@ -5,9 +5,9 @@ require 'controllers/support/shared_user_context' describe Campaigns::CampaignGiftOptionsController, :type => :controller do describe 'authorization' do include_context :shared_user_context - describe 'reject unauthorized users' do + describe 'accept all' do describe 'index' do - include_context :open_to_campaign_editor, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign end end end From 964aadbfb17582a1f0b862ade81e30f59ef5b9f6 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 15:33:41 -0600 Subject: [PATCH 022/440] Correct bug in maintenance_spec.tb --- app/controllers/users/sessions_controller.rb | 2 +- spec/requests/maintenance_spec.rb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index d9a4ad10..4ffebf7e 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Users::SessionsController < Devise::SessionsController layout 'layouts/apified', only: :new - + respond_to :json, only: :new def new @theme = 'minimal' diff --git a/spec/requests/maintenance_spec.rb b/spec/requests/maintenance_spec.rb index 407558bc..0a79ef56 100644 --- a/spec/requests/maintenance_spec.rb +++ b/spec/requests/maintenance_spec.rb @@ -85,7 +85,7 @@ describe 'Maintenance Mode' do it 'allows sign_in.json' do get(:new, {maintenance_token: "#{token}", format: 'json'}) - expect(response.code).to eq '406' + expect(response.code).to eq '200' end end end From cc24183cb958be96b976bae4c7fc85004b707e26 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 15:52:37 -0600 Subject: [PATCH 023/440] Fix some quirky test issues with non-reloaded entities --- spec/lib/update/update_charges_spec.rb | 5 +++++ spec/lib/update/update_payouts_spec.rb | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/spec/lib/update/update_charges_spec.rb b/spec/lib/update/update_charges_spec.rb index 96807c27..6b4606ea 100644 --- a/spec/lib/update/update_charges_spec.rb +++ b/spec/lib/update/update_charges_spec.rb @@ -23,6 +23,11 @@ describe UpdateCharges do let!(:refunds) { [force_create(:refund, charge: charges.last, payment: reverse_payment_for_refund, disbursed: true)]} before(:each) { UpdateCharges.reverse_disburse_all_with_payments([payment_to_reverse.id, payment_to_reverse_2.id, payment_to_reverse_with_refund.id, reverse_payment_for_refund.id]) + + payment_to_reverse.reload + payment_to_reverse_2.reload + payment_to_reverse_with_refund.reload + payment_to_ignore.reload } it 'reverses payments it should' do diff --git a/spec/lib/update/update_payouts_spec.rb b/spec/lib/update/update_payouts_spec.rb index 3ba466d6..27346611 100644 --- a/spec/lib/update/update_payouts_spec.rb +++ b/spec/lib/update/update_payouts_spec.rb @@ -65,6 +65,11 @@ describe UpdatePayouts do payout.payments.push(payment_to_reverse_with_refund) payout.payments.push(reverse_payment_for_refund) UpdatePayouts.reverse_with_stripe(payout.id, bad_status, bad_failure_message) + payment_to_reverse.reload + payment_to_reverse_2.reload + payment_to_reverse_with_refund.reload + reverse_payment_for_refund.reload + payment_to_ignore.reload } it 'reverses proper payments' do From a05f8582de425ccf780bd8e16f2010cb62059c14 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 16:03:08 -0600 Subject: [PATCH 024/440] Remove deprecated '.to_time_in_current_zone' usage --- spec/lib/update/update_donation_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/lib/update/update_donation_spec.rb b/spec/lib/update/update_donation_spec.rb index 16ab3eba..19bd7d4d 100644 --- a/spec/lib/update/update_donation_spec.rb +++ b/spec/lib/update/update_donation_spec.rb @@ -285,7 +285,7 @@ describe UpdateDonation do expect(Payment.count).to eq 1 - expected_offsite_payment= offsite_payment.attributes.merge({check_number:new_check_number, date: new_date.to_time_in_current_zone, gross_amount: new_amount, updated_at: Time.now}).with_indifferent_access + expected_offsite_payment= offsite_payment.attributes.merge({check_number:new_check_number, date: new_date.in_time_zone, gross_amount: new_amount, updated_at: Time.now}).with_indifferent_access offsite_payment.reload expect(offsite_payment.attributes).to eq expected_offsite_payment @@ -351,7 +351,7 @@ describe UpdateDonation do result = UpdateDonation.update_payment(donation.id, blank_data) expected_donation = donation.attributes.merge({ - date: new_date.to_time_in_current_zone, + date: new_date.in_time_zone, amount: new_amount, designation: '', @@ -367,13 +367,13 @@ describe UpdateDonation do donation.reload expect(donation.attributes).to eq expected_donation - expected_p1 = payment.attributes.merge({towards: '', updated_at: Time.now, date: new_date.to_time_in_current_zone, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access + expected_p1 = payment.attributes.merge({towards: '', updated_at: Time.now, date: new_date.in_time_zone, gross_amount: new_amount, fee_total: new_fee, net_amount: new_amount-new_fee}).with_indifferent_access payment.reload expect(payment.attributes).to eq expected_p1 expect(Payment.count).to eq 1 - expected_offsite_payment= offsite_payment.attributes.merge({check_number:'', date: new_date.to_time_in_current_zone, gross_amount: new_amount, updated_at: Time.now}).with_indifferent_access + expected_offsite_payment= offsite_payment.attributes.merge({check_number:'', date: new_date.in_time_zone, gross_amount: new_amount, updated_at: Time.now}).with_indifferent_access offsite_payment.reload expect(offsite_payment.attributes).to eq expected_offsite_payment From dd53f11be94b188e5ab5acc5e1f68b59b5152398 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 16:28:22 -0600 Subject: [PATCH 025/440] Fix wrap parameters bug --- config/initializers/wrap_parameters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 97b340a1..91d9a5eb 100755 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -11,5 +11,5 @@ end # To enable root element in JSON for ActiveRecord objects. ActiveSupport.on_load(:active_record) do - self.include_root_in_json = true + self.include_root_in_json = false end From d90d2b35059528aad3837b9ee563c2763ffd7715 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 16:33:39 -0600 Subject: [PATCH 026/440] Make CreateCampaign fail in pending --- spec/lib/create/create_campaign_spec.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/spec/lib/create/create_campaign_spec.rb b/spec/lib/create/create_campaign_spec.rb index a44170a5..94163ccb 100644 --- a/spec/lib/create/create_campaign_spec.rb +++ b/spec/lib/create/create_campaign_spec.rb @@ -4,5 +4,6 @@ require 'rails_helper' describe CreateCampaign do it 'is untested' do pending 'add tests here' + fail end end \ No newline at end of file From 07298a8cd04324ebe5890650fbd5477fde6afce8 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Mon, 28 Jan 2019 17:18:02 -0600 Subject: [PATCH 027/440] Rails v4.2 --- Gemfile | 4 +-- Gemfile.lock | 74 ++++++++++++++++++++++++++++++++-------------------- 2 files changed, 48 insertions(+), 30 deletions(-) diff --git a/Gemfile b/Gemfile index cd0d63c9..2ad39ee4 100755 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' ruby '2.3.7' gem 'rake' -gem 'rails', '~> 4.1' +gem 'rails', '~> 4.2.8' gem 'rails_12factor' # https://stripe.com/docs/api gem 'stripe' @@ -72,7 +72,7 @@ gem 'httparty' # User authentication # https://github.com/plataformatec/devise -gem 'devise' +gem 'devise', '~> 3.5.0' gem 'devise-async' # http://www.rubygeocoder.com/ diff --git a/Gemfile.lock b/Gemfile.lock index 947a66ab..6968910b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -42,14 +42,16 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.0.0) - actionmailer (4.1.16) - actionpack (= 4.1.16) - actionview (= 4.1.16) + actionmailer (4.2.11) + actionpack (= 4.2.11) + actionview (= 4.2.11) + activejob (= 4.2.11) mail (~> 2.5, >= 2.5.4) - actionpack (4.1.16) - actionview (= 4.1.16) - activesupport (= 4.1.16) - rack (~> 1.5.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.11) + actionview (= 4.2.11) + activesupport (= 4.2.11) + rack (~> 1.6) rack-test (~> 0.6.2) activemodel (4.0.0) activesupport (= 4.0.0) @@ -66,7 +68,7 @@ GEM addressable (2.3.8) amq-protocol (2.2.0) andand (1.3.3) - arel (5.0.1.20140414130214) + arel (6.0.4) ast (2.4.0) aws-eventstream (1.0.1) aws-partitions (1.110.0) @@ -132,6 +134,7 @@ GEM unicode_utils (~> 1.4) crack (0.4.3) safe_yaml (~> 1.0.0) + crass (1.0.4) css_parser (1.6.0) addressable dalli (2.7.9) @@ -203,8 +206,8 @@ GEM railties (>= 3.0.0) faraday (0.11.0) multipart-post (>= 1.2, < 3) - faraday_middleware (0.9.1) - faraday (>= 0.7.4, < 0.10) + faraday_middleware (0.13.0) + faraday (>= 0.7.4, < 1.0) font_assets (0.1.14) rack foreman (0.85.0) @@ -215,6 +218,8 @@ GEM hashie (>= 2.0, < 4.0) geocoder (1.5.0) get_process_mem (0.2.3) + globalid (0.4.2) + activesupport (>= 4.2.0) grape (1.1.0) activesupport builder @@ -262,6 +267,9 @@ GEM activesupport (>= 4) railties (>= 4) request_store (~> 1.0) + loofah (2.2.3) + crass (~> 1.0.2) + nokogiri (>= 1.5.9) mail (2.7.1) mini_mime (>= 0.1.1) mail_view (2.0.4) @@ -304,7 +312,7 @@ GEM puma (>= 2.7, < 4) rabl (0.14.0) activesupport (>= 2.3.14) - rack (1.5.5) + rack (1.6.11) rack-accept (0.4.5) rack (>= 0.4) rack-attack (5.4.2) @@ -314,16 +322,25 @@ GEM rack-test (0.6.3) rack (>= 1.0) rack-timeout (0.5.1) - rails (4.1.16) - actionmailer (= 4.1.16) - actionpack (= 4.1.16) - actionview (= 4.1.16) - activemodel (= 4.1.16) - activerecord (= 4.1.16) - activesupport (= 4.1.16) + rails (4.2.11) + actionmailer (= 4.2.11) + actionpack (= 4.2.11) + actionview (= 4.2.11) + activejob (= 4.2.11) + activemodel (= 4.2.11) + activerecord (= 4.2.11) + activesupport (= 4.2.11) bundler (>= 1.3.0, < 2.0) - railties (= 4.1.16) - sprockets-rails (~> 2.0) + railties (= 4.2.11) + sprockets-rails + rails-deprecated_sanitizer (1.0.3) + activesupport (>= 4.2.0.alpha) + rails-dom-testing (1.0.9) + activesupport (>= 4.2.0, < 5.0) + nokogiri (~> 1.6) + rails-deprecated_sanitizer (>= 1.0.1) + rails-html-sanitizer (1.0.4) + loofah (~> 2.2, >= 2.2.2) rails-i18n (4.0.9) i18n (~> 0.7) railties (~> 4.0) @@ -343,8 +360,9 @@ GEM request_store (1.4.1) rack (>= 1.4) require_all (2.0.0) - responders (1.1.2) - railties (>= 3.2, < 4.2) + responders (2.4.1) + actionpack (>= 4.2.0, < 6.0) + railties (>= 4.2.0, < 6.0) rest-client (2.0.2) http-cookie (>= 1.0.2, < 2.0) mime-types (>= 1.16, < 4.0) @@ -418,10 +436,10 @@ GEM sprockets (3.7.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.2.1) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) stripe (1.58.0) rest-client (>= 1.4, < 4.0) table_print (1.5.6) @@ -477,7 +495,7 @@ DEPENDENCIES database_cleaner debase delayed_job_active_record - devise + devise (~> 3.5.0) devise-async dotenv-rails dry-validation @@ -514,7 +532,7 @@ DEPENDENCIES rack-attack rack-ssl rack-timeout - rails (~> 4.1) + rails (~> 4.2.8) rails-i18n (~> 4.0) rails_12factor rake From 938821ed5f173db0c9db8acd3bd45e48ecbc78b4 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 29 Jan 2019 14:13:52 -0600 Subject: [PATCH 028/440] Initial rails 4.2 --- Gemfile | 6 +- Gemfile.lock | 77 ++++++++++++------- bin/rails | 2 +- bin/setup | 29 +++++++ config/application.rb | 4 +- config/environments/development.rb | 6 ++ config/environments/production.rb | 8 +- config/environments/test.rb | 10 ++- config/initializers/assets.rb | 3 + config/initializers/cookies_serializer.rb | 2 +- config/routes.rb | 2 +- spec/controllers/nonprofits/donations_spec.rb | 19 ++--- 12 files changed, 116 insertions(+), 52 deletions(-) create mode 100755 bin/setup diff --git a/Gemfile b/Gemfile index 2ad39ee4..0704c524 100755 --- a/Gemfile +++ b/Gemfile @@ -30,9 +30,6 @@ gem 'aws-sdk', '~> 1' # for blocking ip addressses gem 'rack-attack' -# For modularizing javascript -# https://github.com/browserify-rails/browserify-rails -gem 'browserify-rails' gem 'sprockets' # for serving fonts on cdn @@ -75,6 +72,9 @@ gem 'httparty' gem 'devise', '~> 3.5.0' gem 'devise-async' +# https://github.com/airbrake/airbrake +gem 'airbrake', '~> 8.0.1' + # http://www.rubygeocoder.com/ gem 'geocoder' # for adding latitude and longitude to location-based tables diff --git a/Gemfile.lock b/Gemfile.lock index 6968910b..a63ce2f5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -53,20 +53,36 @@ GEM activesupport (= 4.2.11) rack (~> 1.6) rack-test (~> 0.6.2) - activemodel (4.0.0) - activesupport (= 4.0.0) - builder (~> 3.1.0) - activerecord (4.0.0) - activemodel (= 4.0.0) - activerecord-deprecated_finders (~> 1.0.2) - activesupport (= 4.0.0) - arel (~> 4.0.0) - activerecord-deprecated_finders (1.0.4) - activesupport (4.0.0) - i18n (~> 0.6, >= 0.6.4) - multi_json (~> 1.0) - addressable (2.3.8) - amq-protocol (2.2.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionview (4.2.11) + activesupport (= 4.2.11) + builder (~> 3.1) + erubis (~> 2.7.0) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (4.2.11) + activesupport (= 4.2.11) + globalid (>= 0.3.0) + activemodel (4.2.11) + activesupport (= 4.2.11) + builder (~> 3.1) + activerecord (4.2.11) + activemodel (= 4.2.11) + activesupport (= 4.2.11) + arel (~> 6.0) + activesupport (4.2.11) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.5.2) + public_suffix (>= 2.0.2, < 4.0) + airbrake (8.0.1) + airbrake-ruby (~> 3.0) + airbrake-ruby (3.1.0) + tdigest (= 0.1.1) + amq-protocol (2.3.0) andand (1.3.3) arel (6.0.4) ast (2.4.0) @@ -105,8 +121,6 @@ GEM debug_inspector (>= 0.0.1) bootsnap (1.3.2) msgpack (~> 1.0) - browserify-rails (1.1.0) - railties (>= 4.0.0, < 5.0) builder (3.2.3) bunny (2.12.0) amq-protocol (~> 2.3, >= 2.3.0) @@ -276,12 +290,16 @@ GEM tilt memcachier (0.0.2) method_source (0.9.1) - mime-types (1.25.1) - mini_magick (4.9.5) - mini_portile2 (2.1.0) - money (6.10.0) - i18n (>= 0.6.4, < 1.0) - msgpack (1.2.0) + mime-types (3.2.2) + mime-types-data (~> 3.2015) + mime-types-data (3.2018.0812) + mini_magick (4.9.2) + mini_mime (1.0.1) + mini_portile2 (2.4.0) + minitest (5.11.3) + money (6.13.1) + i18n (>= 0.6.4, <= 2) + msgpack (1.2.4) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -293,10 +311,10 @@ GEM kdtree require_all netrc (0.11.0) - nokogiri (1.8.5) - mini_portile2 (~> 2.3.0) + nokogiri (1.10.1) + mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) - parallel (1.12.1) + parallel (1.13.0) parser (2.6.0.0) ast (~> 2.4.0) pg (0.21.0) @@ -357,6 +375,7 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (3.0.0) rake (12.3.2) + rbtree (0.4.2) request_store (1.4.1) rack (>= 1.4) require_all (2.0.0) @@ -422,7 +441,7 @@ GEM simplecov-html (~> 0.10.0) simplecov-html (0.10.2) sixarm_ruby_unaccent (1.2.0) - solargraph (0.31.1) + solargraph (0.31.2) backport (~> 0.3) htmlentities (~> 4.3, >= 4.3.4) jaro_winkler (~> 1.5) @@ -443,6 +462,8 @@ GEM stripe (1.58.0) rest-client (>= 1.4, < 4.0) table_print (1.5.6) + tdigest (0.1.1) + rbtree (~> 0.4.2) test-unit (3.2.8) power_assert thor (0.19.4) @@ -479,11 +500,11 @@ PLATFORMS DEPENDENCIES action_mailer_matchers - aws-sdk + airbrake (~> 8.0.1) + aws-sdk (~> 1) aws-ses binding_of_caller bootsnap - browserify-rails bunny (>= 2.6.3) carrierwave carrierwave-aws diff --git a/bin/rails b/bin/rails index 728cd85a..5191e692 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../../config/application', __FILE__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/setup b/bin/setup new file mode 100755 index 00000000..acdb2c13 --- /dev/null +++ b/bin/setup @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +Dir.chdir APP_ROOT do + # This script is a starting point to setup your application. + # Add necessary setup steps to this file: + + puts "== Installing dependencies ==" + system "gem install bundler --conservative" + system "bundle check || bundle install" + + # puts "\n== Copying sample files ==" + # unless File.exist?("config/database.yml") + # system "cp config/database.yml.sample config/database.yml" + # end + + puts "\n== Preparing database ==" + system "bin/rake db:setup" + + puts "\n== Removing old logs and tempfiles ==" + system "rm -f log/*" + system "rm -rf tmp/cache" + + puts "\n== Restarting application server ==" + system "touch tmp/restart.txt" +end diff --git a/config/application.rb b/config/application.rb index 32ee9cec..5e3539ca 100755 --- a/config/application.rb +++ b/config/application.rb @@ -79,9 +79,11 @@ module Commitchange config.i18n.enforce_available_locales = false + config.active_record.raise_in_transactional_callbacks = true + # Add trailing slashes to all routes # config.action_controller.default_url_options = {:trailing_slash => true} # - config.browserify_rails.commandline_options = "-t [ babelify --presets es2015 ]" + # config.browserify_rails.commandline_options = "-t [ babelify --presets es2015 ]" end end diff --git a/config/environments/development.rb b/config/environments/development.rb index eb0ad2c7..58af5b4a 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -49,6 +49,12 @@ Rails.application.configure do # Expands the lines which load the assets config.assets.debug = true + config.assets.digest = true + # Adds additional error checking when serving assets at runtime. + # Checks for improperly declared sprockets dependencies. + # Raises helpful error messages. + config.assets.raise_runtime_errors = true + config.log_level = :debug config.dependency_loading = true if $rails_rake_task diff --git a/config/environments/production.rb b/config/environments/production.rb index b93a877c..de874dbf 100755 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -19,8 +19,9 @@ Rails.application.configure do # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable Rails's static asset server (Apache or nginx will already do this). - config.serve_static_assets = false + # Disable serving static files from the `/public` folder by default since + # Apache or NGINX already handles this. + config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier @@ -67,9 +68,6 @@ Rails.application.configure do # Send deprecation notices to registered listeners. config.active_support.deprecation = :notify - # Disable automatic flushing of the log to improve performance. - # config.autoflush_log = false - # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new diff --git a/config/environments/test.rb b/config/environments/test.rb index fb680fe0..370aecb7 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -5,15 +5,15 @@ Commitchange::Application.configure do # test suite. You never need to work with it otherwise. Remember that # your test database is "scratch space" for the test suite and is wiped # and recreated between test runs. Don't rely on the data there! - config.cache_classes = false + config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application # just for the purpose of running a single test. If you are using a tool that # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static asset server for tests with Cache-Control for performance. - config.serve_static_assets = true + # Configure static file server for tests with Cache-Control for performance. + config.serve_static_files = true config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. @@ -32,6 +32,10 @@ Commitchange::Application.configure do config.action_mailer.delivery_method = :test config.action_mailer.raise_delivery_errors = false + # Randomize the order test cases are executed. + config.active_support.test_order = :random + + # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index d2f4ec33..01ef3e66 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -3,6 +3,9 @@ # Version of your assets, change this if you want to expire all your assets. Rails.application.config.assets.version = '1.0' +# Add additional assets to the asset load path +# Rails.application.config.assets.paths << Emoji.images_path + # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. # Rails.application.config.assets.precompile += %w( search.js ) diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 7a06a89f..7f70458d 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,3 @@ # Be sure to restart your server when you modify this file. -Rails.application.config.action_dispatch.cookies_serializer = :json \ No newline at end of file +Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/routes.rb b/config/routes.rb index b9265366..32c60eb3 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -103,7 +103,7 @@ Rails.application.routes.draw do resource(:miscellaneous_np_info, {only: [:show, :update]}) namespace(:button) do - root({to: :advanced}) + root({action: :advanced}) get(:basic) get(:guided) get(:advanced) diff --git a/spec/controllers/nonprofits/donations_spec.rb b/spec/controllers/nonprofits/donations_spec.rb index f2cdb4f7..79614910 100644 --- a/spec/controllers/nonprofits/donations_spec.rb +++ b/spec/controllers/nonprofits/donations_spec.rb @@ -32,18 +32,19 @@ describe Nonprofits::DonationsController, :type => :controller do end end -describe 'Nonprofits::DonationsController::create_offsite', :type => :request do +describe '.create_offsite', :type => :request do describe 'create_offsite' do include_context :shared_donation_charge_context - include_context :new_controller_user_context + include_context :general_shared_user_context + require 'support/contexts/general_shared_user_context.rb' - it 'reject non-campaign editors (and np authorized folks)' do - run_authorization_tests({method: :post, action: "/nonprofits/#{nonprofit.id}/donations/create_offsite", - successful_users: roles__open_to_campaign_editor}) do |_| - {nonprofit_id: nonprofit.id, - donation: {campaign_id: campaign.id}} - end - end + # it 'reject non-campaign editors (and np authorized folks)', :type => :request do + # run_authorization_tests({method: :post, action: "/nonprofits/#{nonprofit.id}/donations/create_offsite", + # successful_users: roles__open_to_campaign_editor}) do |_| + # {nonprofit_id: nonprofit.id, + # donation: {campaign_id: campaign.id}} + # end + # end #include_context :open_to_np_associate, :post, :create_offsite, nonprofit_id: :__our_np end end \ No newline at end of file From 133986cf40ac39b92ffdb1592119866944302425 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 29 Jan 2019 15:25:34 -0600 Subject: [PATCH 029/440] Update to Ruby 2.4 --- .ruby-version | 2 +- Gemfile | 2 +- docker/build/Dockerfile | 2 +- docker/debug/Dockerfile | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.ruby-version b/.ruby-version index 00355e29..59aa62c1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.7 +2.4.5 diff --git a/Gemfile b/Gemfile index 0704c524..65b26f6b 100755 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby '2.3.7' +ruby '2.4.5' gem 'rake' gem 'rails', '~> 4.2.8' gem 'rails_12factor' diff --git a/docker/build/Dockerfile b/docker/build/Dockerfile index 0440e52f..6ae5cf0a 100644 --- a/docker/build/Dockerfile +++ b/docker/build/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3.7-stretch +FROM ruby:2.4.5-stretch ARG USER RUN mkdir /myapp COPY script/build/debian/prebuild.sh myapp/script/build/debian/prebuild.sh diff --git a/docker/debug/Dockerfile b/docker/debug/Dockerfile index e9c3469c..c9e5c3c6 100644 --- a/docker/debug/Dockerfile +++ b/docker/debug/Dockerfile @@ -1,4 +1,4 @@ -FROM ruby:2.3.7-stretch +FROM ruby:2.4.5-stretch ARG USER RUN mkdir /myapp COPY script/build/debian/prebuild.sh myapp/script/build/debian/prebuild.sh From 69cb2cbc5c89650d3b53aec2e2947e913210faa8 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Thu, 31 Jan 2019 17:11:34 -0600 Subject: [PATCH 030/440] Rails 5.0 builds --- Gemfile | 15 +++-- Gemfile.lock | 163 ++++++++++++++++++++++++--------------------------- 2 files changed, 85 insertions(+), 93 deletions(-) diff --git a/Gemfile b/Gemfile index 65b26f6b..b53407da 100755 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' ruby '2.4.5' gem 'rake' -gem 'rails', '~> 4.2.8' +gem 'rails', '= 5.0.0' gem 'rails_12factor' # https://stripe.com/docs/api gem 'stripe' @@ -69,7 +69,7 @@ gem 'httparty' # User authentication # https://github.com/plataformatec/devise -gem 'devise', '~> 3.5.0' +gem 'devise', '~> 4.4' gem 'devise-async' # https://github.com/airbrake/airbrake @@ -90,7 +90,7 @@ gem 'table_print' gem 'bunny', '>= 2.6.3' -gem 'rails-i18n', '~> 4.0' # For 3.x +gem 'rails-i18n' gem 'i18n-js' gem 'countries' @@ -136,10 +136,15 @@ gem 'dry-validation' # used only for config validation gem 'foreman' -gem 'grape', '~> 1.1.0' +gem 'grape' gem 'grape-entity' gem 'grape-swagger' gem 'grape-swagger-entity' gem 'grape_url_validator' gem 'grape_logging' -gem 'grape_devise', path: 'gems/grape_devise' +gem 'grape_devise' +#gem 'grape_devise', git: 'https://github.com/ericschultz/grape_devise.git' + +#gem 'protected_attributes' + +gem 'rack-ssl' diff --git a/Gemfile.lock b/Gemfile.lock index a63ce2f5..7b153a83 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,23 +8,6 @@ GIT multi_json (~> 1.0) stripe (>= 1.31.0, <= 1.58.0) -GIT - remote: https://github.com/ruby-grape/grape-entity.git - revision: 0e04aa561373b510c2486282979085eaef2ae663 - ref: 0e04aa561373b510c2486282979085eaef2ae663 - specs: - qx (0.1.1) - activerecord (>= 3.0) - colorize (~> 0.8) - -PATH - remote: gems/grape_devise - specs: - grape_devise (0.1.1) - devise (>= 2.2.8, < 5) - grape (> 0.7) - rails (> 3.2, < 6) - PATH remote: gems/ruby-param-validation specs: @@ -42,39 +25,42 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.0.0) - actionmailer (4.2.11) - actionpack (= 4.2.11) - actionview (= 4.2.11) - activejob (= 4.2.11) + actioncable (5.0.0) + actionpack (= 5.0.0) + nio4r (~> 1.2) + websocket-driver (~> 0.6.1) + actionmailer (5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) mail (~> 2.5, >= 2.5.4) - rails-dom-testing (~> 1.0, >= 1.0.5) - actionpack (4.2.11) - actionview (= 4.2.11) - activesupport (= 4.2.11) - rack (~> 1.6) - rack-test (~> 0.6.2) - rails-dom-testing (~> 1.0, >= 1.0.5) + rails-dom-testing (~> 2.0) + actionpack (5.0.0) + actionview (= 5.0.0) + activesupport (= 5.0.0) + rack (~> 2.0) + rack-test (~> 0.6.3) + rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (4.2.11) - activesupport (= 4.2.11) + actionview (5.0.0) + activesupport (= 5.0.0) builder (~> 3.1) erubis (~> 2.7.0) - rails-dom-testing (~> 1.0, >= 1.0.5) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (4.2.11) - activesupport (= 4.2.11) - globalid (>= 0.3.0) - activemodel (4.2.11) - activesupport (= 4.2.11) - builder (~> 3.1) - activerecord (4.2.11) - activemodel (= 4.2.11) - activesupport (= 4.2.11) - arel (~> 6.0) - activesupport (4.2.11) + rails-dom-testing (~> 2.0) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + activejob (5.0.0) + activesupport (= 5.0.0) + globalid (>= 0.3.6) + activemodel (5.0.0) + activesupport (= 5.0.0) + activerecord (5.0.0) + activemodel (= 5.0.0) + activesupport (= 5.0.0) + arel (~> 7.0) + activesupport (5.0.0) + concurrent-ruby (~> 1.0, >= 1.0.2) i18n (~> 0.7) minitest (~> 5.1) - thread_safe (~> 0.3, >= 0.3.4) tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) @@ -84,7 +70,7 @@ GEM tdigest (= 0.1.1) amq-protocol (2.3.0) andand (1.3.3) - arel (6.0.4) + arel (7.1.4) ast (2.4.0) aws-eventstream (1.0.1) aws-partitions (1.110.0) @@ -166,15 +152,15 @@ GEM delayed_job (>= 3.0, < 5) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.10) + devise (4.5.0) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0, < 6.0) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.10.2) - devise (>= 3.2, < 4.0) + devise-async (1.0.0) + activejob (>= 5.0) + devise (>= 4.0) diff-lcs (1.3) docile (1.3.1) domain_name (0.5.20180417) @@ -311,6 +297,7 @@ GEM kdtree require_all netrc (0.11.0) + nio4r (1.2.1) nokogiri (1.10.1) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) @@ -318,9 +305,9 @@ GEM parser (2.6.0.0) ast (~> 2.4.0) pg (0.21.0) - polyglot (0.3.5) - power_assert (1.1.1) - pry (0.11.3) + power_assert (1.1.3) + powerpack (0.1.2) + pry (0.12.0) coderay (~> 1.1.0) method_source (~> 0.9.0) public_suffix (3.0.3) @@ -330,7 +317,7 @@ GEM puma (>= 2.7, < 4) rabl (0.14.0) activesupport (>= 2.3.14) - rack (1.6.11) + rack (2.0.6) rack-accept (0.4.5) rack (>= 0.4) rack-attack (5.4.2) @@ -340,37 +327,35 @@ GEM rack-test (0.6.3) rack (>= 1.0) rack-timeout (0.5.1) - rails (4.2.11) - actionmailer (= 4.2.11) - actionpack (= 4.2.11) - actionview (= 4.2.11) - activejob (= 4.2.11) - activemodel (= 4.2.11) - activerecord (= 4.2.11) - activesupport (= 4.2.11) + rails (5.0.0) + actioncable (= 5.0.0) + actionmailer (= 5.0.0) + actionpack (= 5.0.0) + actionview (= 5.0.0) + activejob (= 5.0.0) + activemodel (= 5.0.0) + activerecord (= 5.0.0) + activesupport (= 5.0.0) bundler (>= 1.3.0, < 2.0) - railties (= 4.2.11) - sprockets-rails - rails-deprecated_sanitizer (1.0.3) - activesupport (>= 4.2.0.alpha) - rails-dom-testing (1.0.9) - activesupport (>= 4.2.0, < 5.0) - nokogiri (~> 1.6) - rails-deprecated_sanitizer (>= 1.0.1) + railties (= 5.0.0) + sprockets-rails (>= 2.0.0) + rails-dom-testing (2.0.3) + activesupport (>= 4.2.0) + nokogiri (>= 1.6) rails-html-sanitizer (1.0.4) loofah (~> 2.2, >= 2.2.2) - rails-i18n (4.0.9) - i18n (~> 0.7) - railties (~> 4.0) + rails-i18n (5.1.3) + i18n (>= 0.7, < 2) + railties (>= 5.0, < 6) rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging - rails_serve_static_assets (0.0.4) - rails_stdout_logging (0.0.3) - railties (3.2.22.5) - actionpack (= 3.2.22.5) - activesupport (= 3.2.22.5) - rack-ssl (~> 1.3.2) + rails_serve_static_assets (0.0.5) + rails_stdout_logging (0.0.5) + railties (5.0.0) + actionpack (= 5.0.0) + activesupport (= 5.0.0) + method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) rainbow (3.0.0) @@ -486,12 +471,15 @@ GEM coercible (~> 1.0) descendants_tracker (~> 0.0, >= 0.0.3) equalizer (~> 0.0, >= 0.0.9) - warden (1.2.7) - rack (>= 1.0) + warden (1.2.8) + rack (>= 2.0.6) webmock (3.4.2) addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff + websocket-driver (0.6.5) + websocket-extensions (>= 0.1.0) + websocket-extensions (0.1.3) xml-simple (1.1.5) yard (0.9.18) @@ -516,7 +504,7 @@ DEPENDENCIES database_cleaner debase delayed_job_active_record - devise (~> 3.5.0) + devise (~> 4.4) devise-async dotenv-rails dry-validation @@ -526,11 +514,10 @@ DEPENDENCIES foreman fullcontact geocoder - grape (~> 1.1.0) + grape grape-entity grape-swagger grape-swagger-entity - grape_devise! grape_logging grape_url_validator hamster @@ -544,7 +531,7 @@ DEPENDENCIES nearest_time_zone parallel param_validation! - pg + pg (~> 0.11) pry puma puma_worker_killer @@ -553,8 +540,8 @@ DEPENDENCIES rack-attack rack-ssl rack-timeout - rails (~> 4.2.8) - rails-i18n (~> 4.0) + rails (= 5.0.0) + rails-i18n rails_12factor rake roadie-rails @@ -577,7 +564,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 2.3.7p456 + ruby 2.4.5p335 BUNDLED WITH 1.17.3 From e19ee9c63861ae8dee9f950adee7fbbba9e726fd Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 11:54:16 -0600 Subject: [PATCH 031/440] Initial to rails v5 upgrade --- Gemfile | 2 +- Gemfile.lock | 82 +++++++------- bin/rails | 2 +- bin/setup | 29 ++--- bin/update | 29 +++++ config/application.rb | 7 +- config/environment.rb | 4 +- config/environments/development.rb | 88 +++++++++------ config/environments/production.rb | 36 +++++-- config/environments/test.rb | 16 +-- .../application_controller_renderer.rb | 8 ++ config/initializers/cookies_serializer.rb | 2 + config/initializers/locale.rb | 1 + config/initializers/new_framework_defaults.rb | 25 +++++ config/initializers/wrap_parameters.rb | 7 +- config/puma.rb | 100 ++++-------------- config/spring.rb | 6 ++ 17 files changed, 252 insertions(+), 192 deletions(-) create mode 100755 bin/update create mode 100644 config/initializers/application_controller_renderer.rb create mode 100644 config/initializers/new_framework_defaults.rb create mode 100644 config/spring.rb diff --git a/Gemfile b/Gemfile index b53407da..71864899 100755 --- a/Gemfile +++ b/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' ruby '2.4.5' gem 'rake' -gem 'rails', '= 5.0.0' +gem 'rails', '= 5.0.7.1' gem 'rails_12factor' # https://stripe.com/docs/api gem 'stripe' diff --git a/Gemfile.lock b/Gemfile.lock index 7b153a83..3ab3b542 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,41 +25,41 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.0.0) - actioncable (5.0.0) - actionpack (= 5.0.0) - nio4r (~> 1.2) + actioncable (5.0.7.1) + actionpack (= 5.0.7.1) + nio4r (>= 1.2, < 3.0) websocket-driver (~> 0.6.1) - actionmailer (5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) + actionmailer (5.0.7.1) + actionpack (= 5.0.7.1) + actionview (= 5.0.7.1) + activejob (= 5.0.7.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.0) - actionview (= 5.0.0) - activesupport (= 5.0.0) + actionpack (5.0.7.1) + actionview (= 5.0.7.1) + activesupport (= 5.0.7.1) rack (~> 2.0) rack-test (~> 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.0) - activesupport (= 5.0.0) + actionview (5.0.7.1) + activesupport (= 5.0.7.1) builder (~> 3.1) erubis (~> 2.7.0) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - activejob (5.0.0) - activesupport (= 5.0.0) + rails-html-sanitizer (~> 1.0, >= 1.0.3) + activejob (5.0.7.1) + activesupport (= 5.0.7.1) globalid (>= 0.3.6) - activemodel (5.0.0) - activesupport (= 5.0.0) - activerecord (5.0.0) - activemodel (= 5.0.0) - activesupport (= 5.0.0) + activemodel (5.0.7.1) + activesupport (= 5.0.7.1) + activerecord (5.0.7.1) + activemodel (= 5.0.7.1) + activesupport (= 5.0.7.1) arel (~> 7.0) - activesupport (5.0.0) + activesupport (5.0.7.1) concurrent-ruby (~> 1.0, >= 1.0.2) - i18n (~> 0.7) + i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) addressable (2.5.2) @@ -251,7 +251,7 @@ GEM domain_name (~> 0.5) httparty (0.16.2) multi_xml (>= 0.5.2) - i18n (0.9.5) + i18n (1.5.3) concurrent-ruby (~> 1.0) i18n-js (3.1.0) i18n (>= 0.6.6, < 2) @@ -275,7 +275,7 @@ GEM mail_view (2.0.4) tilt memcachier (0.0.2) - method_source (0.9.1) + method_source (0.9.2) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) @@ -285,7 +285,7 @@ GEM minitest (5.11.3) money (6.13.1) i18n (>= 0.6.4, <= 2) - msgpack (1.2.4) + msgpack (1.2.6) multi_json (1.13.1) multi_xml (0.6.0) multipart-post (2.0.0) @@ -297,7 +297,7 @@ GEM kdtree require_all netrc (0.11.0) - nio4r (1.2.1) + nio4r (2.3.1) nokogiri (1.10.1) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) @@ -327,17 +327,17 @@ GEM rack-test (0.6.3) rack (>= 1.0) rack-timeout (0.5.1) - rails (5.0.0) - actioncable (= 5.0.0) - actionmailer (= 5.0.0) - actionpack (= 5.0.0) - actionview (= 5.0.0) - activejob (= 5.0.0) - activemodel (= 5.0.0) - activerecord (= 5.0.0) - activesupport (= 5.0.0) - bundler (>= 1.3.0, < 2.0) - railties (= 5.0.0) + rails (5.0.7.1) + actioncable (= 5.0.7.1) + actionmailer (= 5.0.7.1) + actionpack (= 5.0.7.1) + actionview (= 5.0.7.1) + activejob (= 5.0.7.1) + activemodel (= 5.0.7.1) + activerecord (= 5.0.7.1) + activesupport (= 5.0.7.1) + bundler (>= 1.3.0) + railties (= 5.0.7.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -352,9 +352,9 @@ GEM rails_stdout_logging rails_serve_static_assets (0.0.5) rails_stdout_logging (0.0.5) - railties (5.0.0) - actionpack (= 5.0.0) - activesupport (= 5.0.0) + railties (5.0.7.1) + actionpack (= 5.0.7.1) + activesupport (= 5.0.7.1) method_source rake (>= 0.8.7) thor (>= 0.18.1, < 2.0) @@ -540,7 +540,7 @@ DEPENDENCIES rack-attack rack-ssl rack-timeout - rails (= 5.0.0) + rails (= 5.0.7.1) rails-i18n rails_12factor rake diff --git a/bin/rails b/bin/rails index 5191e692..07396602 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,4 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/setup b/bin/setup index acdb2c13..e620b4da 100755 --- a/bin/setup +++ b/bin/setup @@ -1,29 +1,34 @@ #!/usr/bin/env ruby require 'pathname' +require 'fileutils' +include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) -Dir.chdir APP_ROOT do +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do # This script is a starting point to setup your application. - # Add necessary setup steps to this file: + # Add necessary setup steps to this file. - puts "== Installing dependencies ==" - system "gem install bundler --conservative" - system "bundle check || bundle install" + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') # puts "\n== Copying sample files ==" - # unless File.exist?("config/database.yml") - # system "cp config/database.yml.sample config/database.yml" + # unless File.exist?('config/database.yml') + # cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system "bin/rake db:setup" + system! 'bin/rails db:setup' puts "\n== Removing old logs and tempfiles ==" - system "rm -f log/*" - system "rm -rf tmp/cache" + system! 'bin/rails log:clear tmp:clear' puts "\n== Restarting application server ==" - system "touch tmp/restart.txt" + system! 'bin/rails restart' end diff --git a/bin/update b/bin/update new file mode 100755 index 00000000..a8e4462f --- /dev/null +++ b/bin/update @@ -0,0 +1,29 @@ +#!/usr/bin/env ruby +require 'pathname' +require 'fileutils' +include FileUtils + +# path to your application root. +APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) + +def system!(*args) + system(*args) || abort("\n== Command #{args} failed ==") +end + +chdir APP_ROOT do + # This script is a way to update your development environment automatically. + # Add necessary update steps to this file. + + puts '== Installing dependencies ==' + system! 'gem install bundler --conservative' + system('bundle check') || system!('bundle install') + + puts "\n== Updating database ==" + system! 'bin/rails db:migrate' + + puts "\n== Removing old logs and tempfiles ==" + system! 'bin/rails log:clear tmp:clear' + + puts "\n== Restarting application server ==" + system! 'bin/rails restart' +end diff --git a/config/application.rb b/config/application.rb index 5e3539ca..4d592adc 100755 --- a/config/application.rb +++ b/config/application.rb @@ -1,9 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require File.expand_path('../boot', __FILE__) +require_relative 'boot' require 'rails/all' -Bundler.require *Rails.groups(:assets) if defined?(Bundler) +# Require the gems listed in Gemfile, including any gems +# you've limited to :test, :development, or :production. +Bundler.require(*Rails.groups) + #require File.expand_path('lib/htp') # Hamster Table Print diff --git a/config/environment.rb b/config/environment.rb index 32a0ed66..85fd23e6 100755 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -# Load the rails application -require File.expand_path('../application', __FILE__) +# Load the Rails application +require_relative 'application' Encoding.default_external = Encoding::UTF_8 Encoding.default_internal = Encoding::UTF_8 diff --git a/config/environments/development.rb b/config/environments/development.rb index 58af5b4a..ab9d88d3 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -6,61 +6,81 @@ CarrierWave.configure do |config| end Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb + # Settings specified here will take precedence over those in config/application.rb. - # In the development environment your application's code is reloaded on - # every request. This slows down response time but is perfect for development - # since you don't have to restart the web server when you make code changes. - config.cache_classes = false - config.cache_store = Settings.default.cache_store.to_sym + # In the development environment your application's code is reloaded on + # every request. This slows down response time but is perfect for development + # since you don't have to restart the web server when you make code changes. + config.cache_classes = false # Do not eager load code on boot. config.eager_load = false - # Show full error reports and disable caching - config.consider_all_requests_local = true - config.action_controller.perform_caching = false + + # Show full error reports. + config.consider_all_requests_local = true + + # Enable/disable caching. By default caching is disabled. + if Rails.root.join('tmp/caching-dev.txt').exist? + config.action_controller.perform_caching = true + + config.cache_store = :memory_store + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=172800' + } + else + config.action_controller.perform_caching = false + + config.cache_store = :null_store + end # You can uncomment the following to test our real AWS email server on localhost: - # config.action_mailer.delivery_method = :aws_ses - # config.action_mailer.default_url_options = { host: 'commitchange.com' } - config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym - config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } - config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username - config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password + # config.action_mailer.delivery_method = :aws_ses + # config.action_mailer.default_url_options = { host: 'commitchange.com' } + config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym + config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } + config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username + config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password + + config.action_mailer.default_url_options = { host: Settings.mailer.host } - config.action_mailer.default_url_options = { host: Settings.mailer.host } # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false - # Print deprecation notices to the Rails logger - config.active_support.deprecation = :log - # Only use best-standards-support built into browsers - config.action_dispatch.best_standards_support = :builtin + config.action_mailer.perform_caching = false - # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict + # Print deprecation notices to the Rails logger. + config.active_support.deprecation = :log - # Raise an error on page load if there are pending migrations + # Raise exception on mass assignment protection for Active Record models + config.active_record.mass_assignment_sanitizer = :strict + + # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load - # Do not compress assets - config.assets.compress = false + # Debug mode disables concatenation and preprocessing of assets. + # This option may cause significant delays in view rendering with a large + # number of complex assets. + config.assets.debug = true - # Expands the lines which load the assets - config.assets.debug = true + # Suppress logger output for asset requests. + config.assets.quiet = true - config.assets.digest = true - # Adds additional error checking when serving assets at runtime. + # Adds additional error checking when serving assets at runtime. # Checks for improperly declared sprockets dependencies. # Raises helpful error messages. config.assets.raise_runtime_errors = true - config.log_level = :debug + # Raises error for missing translations + # config.action_view.raise_on_missing_translations = true - config.dependency_loading = true if $rails_rake_task - # Turn this on if you want to mess with code inside /node_modules - # config.browserify_rails.evaluate_node_modules = true + # 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.middleware.use I18n::JS::Middleware + config.log_level = :debug + + config.dependency_loading = true if $rails_rake_task + + config.middleware.use I18n::JS::Middleware end diff --git a/config/environments/production.rb b/config/environments/production.rb index de874dbf..d730f187 100755 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -19,9 +19,9 @@ Rails.application.configure do # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable serving static files from the `/public` folder by default since + # Disable serving static files from thne `/public` folder by default since # Apache or NGINX already handles this. - config.serve_static_files = ENV['RAILS_SERVE_STATIC_FILES'].present? + config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? # Compress JavaScripts and CSS. config.assets.js_compressor = :uglifier @@ -35,27 +35,31 @@ Rails.application.configure do # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb + # Enable serving of images, stylesheets, and JavaScripts from an asset server. + # config.action_controller.asset_host = 'http://assets.example.com' + + # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for Nginx # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - # config.force_ssl = true + config.force_ssl = true # Set to :debug to see everything in the log. config.log_level = :info # Prepend all log lines with the following tags. - # config.log_tags = [ :subdomain, :uuid ] - - # Use a different logger for distributed setups. - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server. - # config.action_controller.asset_host = "http://assets.example.com" + # Use a real queuing backend for Active Job (and separate queues per environment) + # config.active_job.queue_adapter = :resque + # config.active_job.queue_name_prefix = "commitchange_#{Rails.env}" + + config.action_mailer.perform_caching = false # Ignore bad email addresses and do not raise email delivery errors. # Set this to true and configure the email server for immediate delivery to raise delivery errors. @@ -71,6 +75,16 @@ Rails.application.configure do # Use default logging formatter so that PID and timestamp are not suppressed. config.log_formatter = ::Logger::Formatter.new + # Use a different logger for distributed setups. + # require 'syslog/logger' + # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') + + if ENV["RAILS_LOG_TO_STDOUT"].present? + logger = ActiveSupport::Logger.new(STDOUT) + logger.formatter = config.log_formatter + config.logger = ActiveSupport::TaggedLogging.new(logger) + end + # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false end diff --git a/config/environments/test.rb b/config/environments/test.rb index 370aecb7..139c6164 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -12,9 +12,11 @@ Commitchange::Application.configure do # preloads Rails for running tests, you may have to set it to true. config.eager_load = false - # Configure static file server for tests with Cache-Control for performance. - config.serve_static_files = true - config.static_cache_control = 'public, max-age=3600' + # Configure public file server for tests with Cache-Control for performance. + config.public_file_server.enabled = true + config.public_file_server.headers = { + 'Cache-Control' => 'public, max-age=3600' + } # Show full error reports and disable caching. config.consider_all_requests_local = true @@ -26,19 +28,19 @@ Commitchange::Application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + config.action_mailer.perform_caching = false + # Tell Action Mailer not to deliver emails to the real world. # The :test delivery method accumulates sent emails in the # ActionMailer::Base.deliveries array. config.action_mailer.delivery_method = :test config.action_mailer.raise_delivery_errors = false - # Randomize the order test cases are executed. - config.active_support.test_order = :random - - # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr + # Randomize the order test cases are executed. + config.active_support.test_order = :random config.action_mailer.default_url_options = {host: 'houdiniproject.test'} diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb new file mode 100644 index 00000000..89d2efab --- /dev/null +++ b/config/initializers/application_controller_renderer.rb @@ -0,0 +1,8 @@ +# Be sure to restart your server when you modify this file. + +# ActiveSupport::Reloader.to_prepare do +# ApplicationController.renderer.defaults.merge!( +# http_host: 'example.org', +# https: false +# ) +# end diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 7f70458d..5a6a32d3 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ # Be sure to restart your server when you modify this file. +# Specify a serializer for the signed and encrypted cookie jars. +# Valid options are :json, :marshal, and :hybrid. Rails.application.config.action_dispatch.cookies_serializer = :json diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index 12686aa0..44eafa22 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,2 +1,3 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later I18n.available_locales = Settings.available_locales +Rails.application.config.i18n.fallbacks = [I18n.default_locale] diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb new file mode 100644 index 00000000..cbf423a8 --- /dev/null +++ b/config/initializers/new_framework_defaults.rb @@ -0,0 +1,25 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 5.0 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +Rails.application.config.action_controller.raise_on_unfiltered_parameters = true + +# Enable per-form CSRF tokens. Previous versions had false. +Rails.application.config.action_controller.per_form_csrf_tokens = false + +# Enable origin-checking CSRF mitigation. Previous versions had false. +Rails.application.config.action_controller.forgery_protection_origin_check = false + +# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. +# Previous versions had false. +ActiveSupport.to_time_preserves_timezone = false + +# Require `belongs_to` associations by default. Previous versions had false. +Rails.application.config.active_record.belongs_to_required_by_default = false + +# Do not halt callback chains when a callback returns false. Previous versions had true. +ActiveSupport.halt_callback_chains_on_return_false = true diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index 91d9a5eb..ed7b7187 100755 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,4 +1,3 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which @@ -6,10 +5,10 @@ # Enable parameter wrapping for JSON. You can disable this by setting :format to an empty array. ActiveSupport.on_load(:action_controller) do - wrap_parameters format: [:json] if respond_to?(:wrap_parameters) + wrap_parameters format: [:json] end -# To enable root element in JSON for ActiveRecord objects. +To enable root element in JSON for ActiveRecord objects. ActiveSupport.on_load(:active_record) do - self.include_root_in_json = false + self.include_root_in_json = true end diff --git a/config/puma.rb b/config/puma.rb index e12f2abf..5695f3af 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,88 +1,34 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -workers Integer(ENV['WEB_CONCURRENCY'] || 1) -threads 1,1 #not threadsafe yet + +# Puma can serve each request in a thread from an internal thread pool. +# The `threads` method setting takes two numbers a minimum and maximum. +# Any libraries that use thread pools should be configured to match +# the maximum value specified for Puma. Default is set to 5 threads for minimum +# and maximum, this matches the default thread size of Active Record. +# +threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads threads_count, threads_count + preload_app! if ENV['RAILS_ENV'] != 'development' rackup DefaultRackup -port ENV['PORT'] || 5000 -environment ENV['RAILS_ENV'] || 'development' +port ENV.fetch("PORT") { 5000 } +environment ENV.fetch('RAILS_ENV'{ 'development' } + +workers ENV['WEB_CONCURRENCY'].fetch { 1 } on_worker_boot do - ActiveSupport.on_load(:active_record) do - config = ActiveRecord::Base.configurations[Rails.env] || - Rails.application.config.database_configuration[Rails.env] - config['pool'] = ENV['RAILS_MAX_THREADS'] || 1 - ActiveRecord::Base.establish_connection - end - + # ActiveSupport.on_load(:active_record) do + # config = ActiveRecord::Base.configurations[Rails.env] || + # Rails.application.config.database_configuration[Rails.env] + # config['pool'] = ENV['RAILS_MAX_THREADS'] || 1 + # ActiveRecord::Base.establish_connection + # end + ActiveRecord::Base.establish_connection if defined?(ActiveRecord) end -# rackup DefaultRackup -# port ENV['PORT'] || 8080 -# environment ENV['RAILS_ENV'] || 'development' -# tag 'commitchange' -# # workers 2 -# daemonize -# -# # Read environment -# require 'dotenv' -# Dotenv.load ".env" -# @env = ENV['RAILS_ENV'] -# # || 'development' -# Dotenv.load ".env.#{@env}" -# puts ENV['PORT'] -# puts "----------------------- #{@env} -----------------------------------" -# @dir = ENV['PUMADIR'] || ENV['PWD'] -# @port = ENV['PORT'] || 10525 -# -# workers Integer(ENV['WEB_CONCURRENCY'] || 1) -# threads_count = Integer(ENV['RAILS_MAX_THREADS'] || 1) -# preload_app! if ENV['RAILS_ENV'] != 'development' -# -# if heroku? -# threads threads_count, threads_count -# else -# threads 1, threads_count -# end -# -# environment @env || 'development' -# #environment 'production' -# -# before_fork do -# require 'puma_worker_killer' -# PumaWorkerKiller.enable_rolling_restart # Default is every 6 hours -# end -# -# tmp_dir = File.expand_path("./tmp", @dir) -# log_dir = File.expand_path("./log", @dir) -# -# if @port -# port @port -# else -# bind "unix://#{tmp_dir}/sockets/puma.sock" -# end -# -# unless heroku? -# # Pid files -# pidfile "#{tmp_dir}/pids/puma.pid" -# state_path "#{tmp_dir}/pids/puma.state" -# -# # Logging -# -# if ENV['LOG_TO_FILES'] -# puts "log to files #{log_dir}/puma.[stdout|stderr].#{@env}.log" -# stdout_redirect "#{log_dir}/puma.stdout.#{@env}.log", "#{log_dir}/puma.stderr.#{@env}.log", true -# end -# end -# on_worker_boot do -# ActiveSupport.on_load(:active_record) do -# config = ActiveRecord::Base.configurations[Rails.env] || -# Rails.application.config.database_configuration[Rails.env] -# config['pool'] = ENV['RAILS_MAX_THREADS'] || 1 -# ActiveRecord::Base.establish_connection -# end -# end - +# Allow puma to be restarted by `rails restart` command. +plugin :tmp_restart diff --git a/config/spring.rb b/config/spring.rb new file mode 100644 index 00000000..c9119b40 --- /dev/null +++ b/config/spring.rb @@ -0,0 +1,6 @@ +%w( + .ruby-version + .rbenv-vars + tmp/restart.txt + tmp/caching-dev.txt +).each { |path| Spring.watch(path) } From 66e4d26d22c3074c3cc767dead2bad8b9e09a5a6 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 12:58:54 -0600 Subject: [PATCH 032/440] Config cleanup --- config/application.rb | 4 +--- config/initializers/devise_async.rb | 2 +- config/initializers/wrap_parameters.rb | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/config/application.rb b/config/application.rb index 4d592adc..91784071 100755 --- a/config/application.rb +++ b/config/application.rb @@ -56,7 +56,7 @@ module Commitchange # This will create an empty whitelist of attributes available for mass-assignment for all models # in your app. As such, your models will need to explicitly whitelist or blacklist accessible # parameters by using an attr_accessible or attr_protected declaration. - config.active_record.whitelist_attributes = true + #config.active_record.whitelist_attributes = true # Enable the asset pipeline config.assets.enabled = true @@ -82,8 +82,6 @@ module Commitchange config.i18n.enforce_available_locales = false - config.active_record.raise_in_transactional_callbacks = true - # Add trailing slashes to all routes # config.action_controller.default_url_options = {:trailing_slash => true} # diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb index 9d71516a..42d81a4e 100644 --- a/config/initializers/devise_async.rb +++ b/config/initializers/devise_async.rb @@ -1,2 +1,2 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Devise::Async.backend = :delayed_job +#Devise::Async.backend = :delayed_job diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index ed7b7187..c72a9676 100755 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -8,7 +8,7 @@ ActiveSupport.on_load(:action_controller) do wrap_parameters format: [:json] end -To enable root element in JSON for ActiveRecord objects. +# To enable root element in JSON for ActiveRecord objects. ActiveSupport.on_load(:active_record) do self.include_root_in_json = true end From 4071455dee2a37eb28fcec6f497b4a1ef2567bc1 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 13:29:14 -0600 Subject: [PATCH 033/440] rails_12factor is unneeded in Rails5 --- Gemfile | 1 - Gemfile.lock | 18 +++++++++--------- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 71864899..0be6acd4 100755 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,6 @@ source 'https://rubygems.org' ruby '2.4.5' gem 'rake' gem 'rails', '= 5.0.7.1' -gem 'rails_12factor' # https://stripe.com/docs/api gem 'stripe' diff --git a/Gemfile.lock b/Gemfile.lock index 3ab3b542..5a900acc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -8,6 +8,14 @@ GIT multi_json (~> 1.0) stripe (>= 1.31.0, <= 1.58.0) +PATH + remote: gems/grape_devise + specs: + grape_devise (0.1.1) + devise (>= 2.2.8, < 5) + grape (> 0.7) + rails (> 3.2, < 6) + PATH remote: gems/ruby-param-validation specs: @@ -322,8 +330,6 @@ GEM rack (>= 0.4) rack-attack (5.4.2) rack (>= 1.0, < 3) - rack-ssl (1.4.1) - rack rack-test (0.6.3) rack (>= 1.0) rack-timeout (0.5.1) @@ -347,11 +353,6 @@ GEM rails-i18n (5.1.3) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - rails_12factor (0.0.3) - rails_serve_static_assets - rails_stdout_logging - rails_serve_static_assets (0.0.5) - rails_stdout_logging (0.0.5) railties (5.0.7.1) actionpack (= 5.0.7.1) activesupport (= 5.0.7.1) @@ -518,6 +519,7 @@ DEPENDENCIES grape-entity grape-swagger grape-swagger-entity + grape_devise! grape_logging grape_url_validator hamster @@ -538,11 +540,9 @@ DEPENDENCIES qx! rabl rack-attack - rack-ssl rack-timeout rails (= 5.0.7.1) rails-i18n - rails_12factor rake roadie-rails rspec From d3535c0b80ea295c75ccb374ce6ce1de144cc029 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 13:40:24 -0600 Subject: [PATCH 034/440] Models now inherit from ApplicationRecord --- app/models/activity.rb | 2 +- app/models/application_record.rb | 5 +++++ app/models/bank_account.rb | 2 +- app/models/billing_plan.rb | 2 +- app/models/billing_subscription.rb | 2 +- app/models/campaign.rb | 2 +- app/models/campaign_gift.rb | 2 +- app/models/campaign_gift_option.rb | 2 +- app/models/card.rb | 2 +- app/models/charge.rb | 2 +- app/models/comment.rb | 2 +- app/models/coupon.rb | 2 +- app/models/custom_field_join.rb | 2 +- app/models/custom_field_master.rb | 2 +- app/models/direct_debit_detail.rb | 2 +- app/models/dispute.rb | 2 +- app/models/donation.rb | 2 +- app/models/email_draft.rb | 2 +- app/models/email_list.rb | 2 +- app/models/email_setting.rb | 2 +- app/models/event.rb | 2 +- app/models/event_discount.rb | 2 +- app/models/export.rb | 2 +- app/models/full_contact_info.rb | 2 +- app/models/full_contact_org.rb | 2 +- app/models/full_contact_photo.rb | 2 +- app/models/full_contact_social_profile.rb | 2 +- app/models/full_contact_topic.rb | 2 +- app/models/image_attachment.rb | 2 +- app/models/import.rb | 2 +- app/models/miscellaneous_np_info.rb | 2 +- app/models/nonprofit.rb | 2 +- app/models/nonprofit_account.rb | 2 +- app/models/offsite_payment.rb | 2 +- app/models/payment.rb | 2 +- app/models/payment_import.rb | 2 +- app/models/payment_payout.rb | 2 +- app/models/payout.rb | 2 +- app/models/profile.rb | 2 +- app/models/recurring_donation.rb | 2 +- app/models/refund.rb | 2 +- app/models/role.rb | 2 +- app/models/source_token.rb | 2 +- app/models/supporter.rb | 2 +- app/models/supporter_email.rb | 2 +- app/models/supporter_note.rb | 2 +- app/models/tag_join.rb | 2 +- app/models/tag_master.rb | 2 +- app/models/ticket.rb | 2 +- app/models/ticket_level.rb | 2 +- app/models/tracking.rb | 2 +- app/models/user.rb | 2 +- 52 files changed, 56 insertions(+), 51 deletions(-) create mode 100644 app/models/application_record.rb diff --git a/app/models/activity.rb b/app/models/activity.rb index c98f0888..20a763bf 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Activity < ActiveRecord::Base +class Activity < ApplicationRecord end diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 00000000..52fec221 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,5 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true +end \ No newline at end of file diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb index 92265f4f..c618467a 100644 --- a/app/models/bank_account.rb +++ b/app/models/bank_account.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class BankAccount < ActiveRecord::Base +class BankAccount < ApplicationRecord attr_accessible \ :name, # str (readable bank name identifier, eg. "Wells Fargo *1234") diff --git a/app/models/billing_plan.rb b/app/models/billing_plan.rb index 552a273d..217bb747 100644 --- a/app/models/billing_plan.rb +++ b/app/models/billing_plan.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class BillingPlan < ActiveRecord::Base +class BillingPlan < ApplicationRecord Names = ['Starter', 'Fundraising', 'Supporter Management'] DefaultAmounts = [0, 9900, 29900] # in pennies diff --git a/app/models/billing_subscription.rb b/app/models/billing_subscription.rb index 5e67bfea..9d33ee48 100644 --- a/app/models/billing_subscription.rb +++ b/app/models/billing_subscription.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class BillingSubscription < ActiveRecord::Base +class BillingSubscription < ApplicationRecord attr_accessible \ :nonprofit_id, :nonprofit, diff --git a/app/models/campaign.rb b/app/models/campaign.rb index d4037873..c104e0af 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Campaign < ActiveRecord::Base +class Campaign < ApplicationRecord attr_accessible \ :name, diff --git a/app/models/campaign_gift.rb b/app/models/campaign_gift.rb index be8e76ba..273cf9f2 100644 --- a/app/models/campaign_gift.rb +++ b/app/models/campaign_gift.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class CampaignGift < ActiveRecord::Base +class CampaignGift < ApplicationRecord attr_accessible \ :donation_id, diff --git a/app/models/campaign_gift_option.rb b/app/models/campaign_gift_option.rb index f69129c4..7c07e901 100644 --- a/app/models/campaign_gift_option.rb +++ b/app/models/campaign_gift_option.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class CampaignGiftOption < ActiveRecord::Base +class CampaignGiftOption < ApplicationRecord attr_accessible \ :amount_one_time, #int (cents) diff --git a/app/models/card.rb b/app/models/card.rb index d7b806f8..b63a09fe 100755 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Card < ActiveRecord::Base +class Card < ApplicationRecord attr_accessible \ :cardholders_name, # str (name associated with this card) diff --git a/app/models/charge.rb b/app/models/charge.rb index d2ce3f47..7b300efa 100644 --- a/app/models/charge.rb +++ b/app/models/charge.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # A Charge represents a potential debit to a nonprofit's account on a credit card donation action. -class Charge < ActiveRecord::Base +class Charge < ApplicationRecord attr_accessible \ :amount, diff --git a/app/models/comment.rb b/app/models/comment.rb index e0491d49..6f4b9e52 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Comment < ActiveRecord::Base +class Comment < ApplicationRecord attr_accessible \ :host_id, :host_type, #parent: Event, Campaign, nil diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 1377d548..08d93729 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Coupon < ActiveRecord::Base +class Coupon < ApplicationRecord attr_accessible \ :name, :victim_np_id, diff --git a/app/models/custom_field_join.rb b/app/models/custom_field_join.rb index c38e9331..0d96a378 100644 --- a/app/models/custom_field_join.rb +++ b/app/models/custom_field_join.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class CustomFieldJoin < ActiveRecord::Base +class CustomFieldJoin < ApplicationRecord attr_accessible \ :supporter, :supporter_id, diff --git a/app/models/custom_field_master.rb b/app/models/custom_field_master.rb index ef6bb85c..56f4398a 100644 --- a/app/models/custom_field_master.rb +++ b/app/models/custom_field_master.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class CustomFieldMaster < ActiveRecord::Base +class CustomFieldMaster < ApplicationRecord attr_accessible \ :nonprofit, :nonprofit_id, diff --git a/app/models/direct_debit_detail.rb b/app/models/direct_debit_detail.rb index ba8fbe67..d4dbfdcb 100644 --- a/app/models/direct_debit_detail.rb +++ b/app/models/direct_debit_detail.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class DirectDebitDetail < ActiveRecord::Base +class DirectDebitDetail < ApplicationRecord attr_accessible :iban, :account_holder_name, :bic, :supporter_id, :holder has_many :donations diff --git a/app/models/dispute.rb b/app/models/dispute.rb index 722768b3..e8b912e4 100644 --- a/app/models/dispute.rb +++ b/app/models/dispute.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Dispute < ActiveRecord::Base +class Dispute < ApplicationRecord Reasons = [:unrecognized, :duplicate, :fraudulent, :subscription_canceled, :product_unacceptable, :product_not_received, :unrecognized, :credit_not_processed, :goods_services_returned_or_refused, :goods_services_cancelled, :incorrect_account_details, :insufficient_funds, :bank_cannot_process, :debit_not_authorized, :general] diff --git a/app/models/donation.rb b/app/models/donation.rb index 43f995c9..1c7053d8 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Donation < ActiveRecord::Base +class Donation < ApplicationRecord attr_accessible \ :date, # datetime (when this donation was made) diff --git a/app/models/email_draft.rb b/app/models/email_draft.rb index 9614c13c..cc4951b0 100644 --- a/app/models/email_draft.rb +++ b/app/models/email_draft.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class EmailDraft < ActiveRecord::Base +class EmailDraft < ApplicationRecord attr_accessible \ :nonprofit, :nonprofit_id, diff --git a/app/models/email_list.rb b/app/models/email_list.rb index ea64e773..4f8fccf1 100644 --- a/app/models/email_list.rb +++ b/app/models/email_list.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class EmailList < ActiveRecord::Base +class EmailList < ApplicationRecord attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master belongs_to :nonprofit belongs_to :tag_master diff --git a/app/models/email_setting.rb b/app/models/email_setting.rb index c4f8bf2e..0171f071 100644 --- a/app/models/email_setting.rb +++ b/app/models/email_setting.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class EmailSetting < ActiveRecord::Base +class EmailSetting < ApplicationRecord attr_accessible \ :user_id, :user, diff --git a/app/models/event.rb b/app/models/event.rb index 4face493..37c631f7 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Event < ActiveRecord::Base +class Event < ApplicationRecord attr_accessible \ :deleted, #bool for soft-delete diff --git a/app/models/event_discount.rb b/app/models/event_discount.rb index 7e6a69a0..2888ecaa 100644 --- a/app/models/event_discount.rb +++ b/app/models/event_discount.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class EventDiscount < ActiveRecord::Base +class EventDiscount < ApplicationRecord attr_accessible \ :code, :event_id, diff --git a/app/models/export.rb b/app/models/export.rb index e49d5609..ee4717a4 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Export < ActiveRecord::Base +class Export < ApplicationRecord STATUS = %w[queued started completed failed].freeze attr_accessible :exception, :nonprofit, :status, :user, :export_type, :parameters, :ended, :url, :user_id, :nonprofit_id diff --git a/app/models/full_contact_info.rb b/app/models/full_contact_info.rb index 712579fa..7862d89d 100644 --- a/app/models/full_contact_info.rb +++ b/app/models/full_contact_info.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class FullContactInfo < ActiveRecord::Base +class FullContactInfo < ApplicationRecord attr_accessible \ :email, :full_name, diff --git a/app/models/full_contact_org.rb b/app/models/full_contact_org.rb index a8b4ce1b..05acfc9c 100644 --- a/app/models/full_contact_org.rb +++ b/app/models/full_contact_org.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class FullContactOrg < ActiveRecord::Base +class FullContactOrg < ApplicationRecord attr_accessible \ :name, diff --git a/app/models/full_contact_photo.rb b/app/models/full_contact_photo.rb index 258a764f..b96a5bb7 100644 --- a/app/models/full_contact_photo.rb +++ b/app/models/full_contact_photo.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class FullContactPhoto < ActiveRecord::Base +class FullContactPhoto < ApplicationRecord attr_accessible \ :full_contact_info, :full_contact_info_id, diff --git a/app/models/full_contact_social_profile.rb b/app/models/full_contact_social_profile.rb index 047160ea..b98a4dd3 100644 --- a/app/models/full_contact_social_profile.rb +++ b/app/models/full_contact_social_profile.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class FullContactSocialProfile < ActiveRecord::Base +class FullContactSocialProfile < ApplicationRecord attr_accessible \ :full_contact_info, :full_contact_info_id, diff --git a/app/models/full_contact_topic.rb b/app/models/full_contact_topic.rb index 475f9c27..0839097b 100644 --- a/app/models/full_contact_topic.rb +++ b/app/models/full_contact_topic.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class FullContactTopic < ActiveRecord::Base +class FullContactTopic < ApplicationRecord attr_accessible \ :provider, diff --git a/app/models/image_attachment.rb b/app/models/image_attachment.rb index d6afb9a4..642ba68c 100644 --- a/app/models/image_attachment.rb +++ b/app/models/image_attachment.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class ImageAttachment < ActiveRecord::Base +class ImageAttachment < ApplicationRecord attr_accessible :parent_id, :file mount_uploader :file, ImageAttachmentUploader diff --git a/app/models/import.rb b/app/models/import.rb index b4e3f681..66a06071 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Import < ActiveRecord::Base +class Import < ApplicationRecord attr_accessible \ :user_id, :user, diff --git a/app/models/miscellaneous_np_info.rb b/app/models/miscellaneous_np_info.rb index 2b15029a..a9ba9179 100644 --- a/app/models/miscellaneous_np_info.rb +++ b/app/models/miscellaneous_np_info.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class MiscellaneousNpInfo < ActiveRecord::Base +class MiscellaneousNpInfo < ApplicationRecord attr_accessible \ :donate_again_url, diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index f7280036..b2bda8d4 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Nonprofit < ActiveRecord::Base +class Nonprofit < ApplicationRecord Categories = ["Public Benefit", "Human Services", "Education", "Civic Duty", "Human Rights", "Animals", "Environment", "Health", "Arts, Culture, Humanities", "International", "Children", "Religion", "LGBTQ", "Women's Rights", "Disaster Relief", "Veterans"] diff --git a/app/models/nonprofit_account.rb b/app/models/nonprofit_account.rb index a3bff211..b5ee62d0 100644 --- a/app/models/nonprofit_account.rb +++ b/app/models/nonprofit_account.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class NonprofitAccount < ActiveRecord::Base +class NonprofitAccount < ApplicationRecord attr_accessible \ :stripe_account_id, #str diff --git a/app/models/offsite_payment.rb b/app/models/offsite_payment.rb index 4e4cf03a..9a729ed2 100644 --- a/app/models/offsite_payment.rb +++ b/app/models/offsite_payment.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class OffsitePayment < ActiveRecord::Base +class OffsitePayment < ApplicationRecord attr_accessible :gross_amount, :kind, :date, :check_number belongs_to :payment, dependent: :destroy diff --git a/app/models/payment.rb b/app/models/payment.rb index e916a588..4e622d7b 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -3,7 +3,7 @@ # If connected to a charge, this represents money potentially debited to the nonprofit's account # If connected to an offsite_payment, this is money the nonprofit is recording for convenience. -class Payment < ActiveRecord::Base +class Payment < ApplicationRecord attr_accessible \ :towards, diff --git a/app/models/payment_import.rb b/app/models/payment_import.rb index ebe96e11..fea614a5 100644 --- a/app/models/payment_import.rb +++ b/app/models/payment_import.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class PaymentImport < ActiveRecord::Base +class PaymentImport < ApplicationRecord attr_accessible :nonprofit, :user has_and_belongs_to_many :donations belongs_to :nonprofit diff --git a/app/models/payment_payout.rb b/app/models/payment_payout.rb index a7a840d1..3c686763 100644 --- a/app/models/payment_payout.rb +++ b/app/models/payment_payout.rb @@ -11,7 +11,7 @@ # It's also nice to keep a historical records of fees for individual donations # since our fees will continue to change as our transaction volume increases -class PaymentPayout < ActiveRecord::Base +class PaymentPayout < ApplicationRecord attr_accessible \ :payment_id, :payment, diff --git a/app/models/payout.rb b/app/models/payout.rb index aa53c033..58fd8542 100644 --- a/app/models/payout.rb +++ b/app/models/payout.rb @@ -4,7 +4,7 @@ # # These are tied to Stripe transfers -class Payout < ActiveRecord::Base +class Payout < ApplicationRecord attr_accessible \ :scheduled, # bool (whether this was made automatically at the beginning of the month) diff --git a/app/models/profile.rb b/app/models/profile.rb index 5ff2f392..d9e17c0d 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Profile < ActiveRecord::Base +class Profile < ApplicationRecord attr_accessible \ :registered, # bool diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index 1a088b1e..3e018bbe 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class RecurringDonation < ActiveRecord::Base +class RecurringDonation < ApplicationRecord attr_accessible \ :amount, # int (cents) diff --git a/app/models/refund.rb b/app/models/refund.rb index f7fff991..25e7cf8b 100644 --- a/app/models/refund.rb +++ b/app/models/refund.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Refund < ActiveRecord::Base +class Refund < ApplicationRecord Reasons = [:duplicate, :fraudulent, :requested_by_customer] diff --git a/app/models/role.rb b/app/models/role.rb index 2c8401bf..8bbd5263 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Role < ActiveRecord::Base +class Role < ApplicationRecord Names = [ :super_admin, # global access diff --git a/app/models/source_token.rb b/app/models/source_token.rb index 2d785ce1..1d6ca55f 100644 --- a/app/models/source_token.rb +++ b/app/models/source_token.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class SourceToken < ActiveRecord::Base +class SourceToken < ApplicationRecord self.primary_key = :token attr_accessible :expiration, :token, :max_uses, :total_uses belongs_to :tokenizable, :polymorphic => true diff --git a/app/models/supporter.rb b/app/models/supporter.rb index 8d5a89de..05674b16 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Supporter < ActiveRecord::Base +class Supporter < ApplicationRecord attr_accessible \ :search_vectors, diff --git a/app/models/supporter_email.rb b/app/models/supporter_email.rb index 5ac0d8d9..0e9b2e53 100644 --- a/app/models/supporter_email.rb +++ b/app/models/supporter_email.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class SupporterEmail < ActiveRecord::Base +class SupporterEmail < ApplicationRecord attr_accessible \ :to, :from, diff --git a/app/models/supporter_note.rb b/app/models/supporter_note.rb index dd920f07..ff7d29f9 100644 --- a/app/models/supporter_note.rb +++ b/app/models/supporter_note.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class SupporterNote < ActiveRecord::Base +class SupporterNote < ApplicationRecord attr_accessible \ :content, diff --git a/app/models/tag_join.rb b/app/models/tag_join.rb index 5f0e402a..d531ddb9 100644 --- a/app/models/tag_join.rb +++ b/app/models/tag_join.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class TagJoin < ActiveRecord::Base +class TagJoin < ApplicationRecord attr_accessible \ :supporter, :supporter_id, diff --git a/app/models/tag_master.rb b/app/models/tag_master.rb index 6e9f0ffd..836bdbfb 100644 --- a/app/models/tag_master.rb +++ b/app/models/tag_master.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class TagMaster < ActiveRecord::Base +class TagMaster < ApplicationRecord attr_accessible \ :nonprofit, :nonprofit_id, diff --git a/app/models/ticket.rb b/app/models/ticket.rb index e805ad89..9e56fd2b 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Ticket < ActiveRecord::Base +class Ticket < ApplicationRecord attr_accessible :note, :event_discount, :event_discount_id diff --git a/app/models/ticket_level.rb b/app/models/ticket_level.rb index 9d628782..130157e0 100644 --- a/app/models/ticket_level.rb +++ b/app/models/ticket_level.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class TicketLevel < ActiveRecord::Base +class TicketLevel < ApplicationRecord attr_accessible \ :amount, #integer diff --git a/app/models/tracking.rb b/app/models/tracking.rb index d3a175c7..a730989f 100644 --- a/app/models/tracking.rb +++ b/app/models/tracking.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Tracking < ActiveRecord::Base +class Tracking < ApplicationRecord attr_accessible :utm_campaign, :utm_content, :utm_medium, :utm_source belongs_to :donation diff --git a/app/models/user.rb b/app/models/user.rb index 32f1e674..dffee4cb 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class User < ActiveRecord::Base +class User < ApplicationRecord attr_accessible \ :email, # str: balidated with Devise From a12e4099adff23dbafdb6fe8af18a704289c361b Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 13:42:44 -0600 Subject: [PATCH 035/440] Add ApplicationJob --- app/jobs/application_job.rb | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 app/jobs/application_job.rb diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 00000000..cd5fb472 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +class ApplicationJob < ActiveJob::Base +end From a7e5baae2c8e780e3e4c759fa0668b487c7ed94f Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 1 Feb 2019 13:45:36 -0600 Subject: [PATCH 036/440] Tests are random by default so we don't need to say it --- config/environments/test.rb | 3 --- 1 file changed, 3 deletions(-) diff --git a/config/environments/test.rb b/config/environments/test.rb index 139c6164..20bd8332 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -39,9 +39,6 @@ Commitchange::Application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - # Randomize the order test cases are executed. - config.active_support.test_order = :random - config.action_mailer.default_url_options = {host: 'houdiniproject.test'} # Raises error for missing translations From f2c32fffca27e0f0fdbc9a172cf68137b3148583 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 21 Jun 2019 17:12:54 -0500 Subject: [PATCH 037/440] Initially comment out attr_accessible --- app/models/bank_account.rb | 25 +++--- app/models/billing_plan.rb | 15 ++-- app/models/billing_subscription.rb | 11 +-- app/models/campaign.rb | 71 +++++++++-------- app/models/campaign_gift.rb | 11 +-- app/models/campaign_gift_option.rb | 23 +++--- app/models/card.rb | 23 +++--- app/models/charge.rb | 11 +-- app/models/comment.rb | 9 ++- app/models/custom_field_join.rb | 9 ++- app/models/dispute.rb | 15 ++-- app/models/donation.rb | 39 ++++----- app/models/email_draft.rb | 14 ++-- app/models/email_setting.rb | 17 ++-- app/models/event.rb | 65 +++++++-------- app/models/event_discount.rb | 11 +-- app/models/full_contact_info.rb | 29 +++---- app/models/full_contact_org.rb | 19 ++--- app/models/full_contact_photo.rb | 13 +-- app/models/full_contact_social_profile.rb | 17 ++-- app/models/full_contact_topic.rb | 9 ++- app/models/import.rb | 15 ++-- app/models/miscellaneous_np_info.rb | 7 +- app/models/nonprofit.rb | 97 ++++++++++++----------- app/models/nonprofit_account.rb | 7 +- app/models/payment.rb | 15 ++-- app/models/payment_payout.rb | 11 +-- app/models/payout.rb | 29 +++---- app/models/profile.rb | 35 ++++---- app/models/recurring_donation.rb | 31 ++++---- app/models/refund.rb | 21 ++--- app/models/role.rb | 9 ++- app/models/supporter.rb | 55 ++++++------- app/models/supporter_email.rb | 19 ++--- app/models/supporter_note.rb | 7 +- app/models/tag_join.rb | 7 +- app/models/tag_master.rb | 11 +-- app/models/ticket_level.rb | 23 +++--- app/models/user.rb | 47 +++++------ 39 files changed, 470 insertions(+), 432 deletions(-) diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb index c618467a..b7eedb59 100644 --- a/app/models/bank_account.rb +++ b/app/models/bank_account.rb @@ -1,18 +1,19 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BankAccount < ApplicationRecord - attr_accessible \ - :name, # str (readable bank name identifier, eg. "Wells Fargo *1234") - :confirmation_token, # str (randomly generated private token for email confirmation) - :account_number, # str (last digits only) - :bank_name, # str - :pending_verification, # bool (whether this bank account is still awaiting email confirmation) - :status, # str - :email, # str (contact email associated with the user who created this bank account) - :deleted, # bool (soft delete flag) - :stripe_bank_account_token, # str - :stripe_bank_account_id, # str - :nonprofit_id, :nonprofit + #TODO + # attr_accessible \ + # :name, # str (readable bank name identifier, eg. "Wells Fargo *1234") + # :confirmation_token, # str (randomly generated private token for email confirmation) + # :account_number, # str (last digits only) + # :bank_name, # str + # :pending_verification, # bool (whether this bank account is still awaiting email confirmation) + # :status, # str + # :email, # str (contact email associated with the user who created this bank account) + # :deleted, # bool (soft delete flag) + # :stripe_bank_account_token, # str + # :stripe_bank_account_id, # str + # :nonprofit_id, :nonprofit #validates :stripe_bank_account_token, presence: true, uniqueness: true # validates :stripe_bank_account_id, presence: true, uniqueness: true diff --git a/app/models/billing_plan.rb b/app/models/billing_plan.rb index 217bb747..b5468646 100644 --- a/app/models/billing_plan.rb +++ b/app/models/billing_plan.rb @@ -3,13 +3,14 @@ class BillingPlan < ApplicationRecord Names = ['Starter', 'Fundraising', 'Supporter Management'] DefaultAmounts = [0, 9900, 29900] # in pennies - attr_accessible \ - :name, #str: readable name - :tier, #int: 0-4 (0: Free, 1: Fundraising, 2: Supporter Management) - :amount, #int (cents) - :stripe_plan_id, #str (matches plan ID in Stripe) Not needed if it's not a paying subscription - :interval, #str ('monthly', 'annual') - :percentage_fee # 0.038 + #TODO + # attr_accessible \ + # :name, #str: readable name + # :tier, #int: 0-4 (0: Free, 1: Fundraising, 2: Supporter Management) + # :amount, #int (cents) + # :stripe_plan_id, #str (matches plan ID in Stripe) Not needed if it's not a paying subscription + # :interval, #str ('monthly', 'annual') + # :percentage_fee # 0.038 has_many :billing_subscriptions diff --git a/app/models/billing_subscription.rb b/app/models/billing_subscription.rb index 9d33ee48..85b5679e 100644 --- a/app/models/billing_subscription.rb +++ b/app/models/billing_subscription.rb @@ -1,11 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingSubscription < ApplicationRecord - attr_accessible \ - :nonprofit_id, :nonprofit, - :billing_plan_id, :billing_plan, - :stripe_subscription_id, - :status # trialing, active, past_due, canceled, or unpaid + #TODO + # attr_accessible \ + # :nonprofit_id, :nonprofit, + # :billing_plan_id, :billing_plan, + # :stripe_subscription_id, + # :status # trialing, active, past_due, canceled, or unpaid attr_accessor :stripe_plan_id, :manual belongs_to :nonprofit diff --git a/app/models/campaign.rb b/app/models/campaign.rb index c104e0af..5aaa9162 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -1,41 +1,42 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Campaign < ApplicationRecord - attr_accessible \ - :name, - :tagline, - :slug, # str: url name - :total_supporters, - :goal_amount, - :nonprofit_id, - :profile_id, - :main_image, - :remove_main_image, # for carrierwave - :background_image, - :remove_background_image, #bool carrierwave - :banner_image, - :remove_banner_image, - :published, - :video_url, #str - :vimeo_video_id, - :youtube_video_id, - :summary, - :recurring_fund, # bool: whether this is a recurring campaign - :body, - :goal_amount_dollars, #accessor: translated into goal_amount (cents) - :show_total_raised, # bool - :show_total_count, # bool - :hide_activity_feed, # bool - :end_datetime, - :deleted, #bool (soft delete) - :hide_goal, # bool - :hide_thermometer, #bool - :hide_title, # bool - :receipt_message, # text - :hide_custom_amounts, # boolean - :parent_campaign_id, - :reason_for_supporting, - :default_reason_for_supporting + #TODO + # attr_accessible \ + # :name, + # :tagline, + # :slug, # str: url name + # :total_supporters, + # :goal_amount, + # :nonprofit_id, + # :profile_id, + # :main_image, + # :remove_main_image, # for carrierwave + # :background_image, + # :remove_background_image, #bool carrierwave + # :banner_image, + # :remove_banner_image, + # :published, + # :video_url, #str + # :vimeo_video_id, + # :youtube_video_id, + # :summary, + # :recurring_fund, # bool: whether this is a recurring campaign + # :body, + # :goal_amount_dollars, #accessor: translated into goal_amount (cents) + # :show_total_raised, # bool + # :show_total_count, # bool + # :hide_activity_feed, # bool + # :end_datetime, + # :deleted, #bool (soft delete) + # :hide_goal, # bool + # :hide_thermometer, #bool + # :hide_title, # bool + # :receipt_message, # text + # :hide_custom_amounts, # boolean + # :parent_campaign_id, + # :reason_for_supporting, + # :default_reason_for_supporting validate :end_datetime_cannot_be_in_past, :on => :create validates :profile, :presence => true diff --git a/app/models/campaign_gift.rb b/app/models/campaign_gift.rb index 273cf9f2..d08a59da 100644 --- a/app/models/campaign_gift.rb +++ b/app/models/campaign_gift.rb @@ -1,11 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGift < ApplicationRecord - attr_accessible \ - :donation_id, - :donation, - :campaign_gift_option, - :campaign_gift_option_id + #TODO + # attr_accessible \ + # :donation_id, + # :donation, + # :campaign_gift_option, + # :campaign_gift_option_id belongs_to :donation belongs_to :campaign_gift_option diff --git a/app/models/campaign_gift_option.rb b/app/models/campaign_gift_option.rb index 7c07e901..187010d1 100644 --- a/app/models/campaign_gift_option.rb +++ b/app/models/campaign_gift_option.rb @@ -1,17 +1,18 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGiftOption < ApplicationRecord - attr_accessible \ - :amount_one_time, #int (cents) - :amount_recurring, #int (cents) - :amount_dollars, #str, gets converted to amount - :description, # text - :name, # str - :campaign, #assocation - :quantity, #int (optional) - :to_ship, #boolean - :order, #int (optional) - :hide_contributions #boolean (optional) + #TODO + # attr_accessible \ + # :amount_one_time, #int (cents) + # :amount_recurring, #int (cents) + # :amount_dollars, #str, gets converted to amount + # :description, # text + # :name, # str + # :campaign, #assocation + # :quantity, #int (optional) + # :to_ship, #boolean + # :order, #int (optional) + # :hide_contributions #boolean (optional) belongs_to :campaign has_many :campaign_gifts diff --git a/app/models/card.rb b/app/models/card.rb index b63a09fe..0c71e983 100755 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,17 +1,18 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Card < ApplicationRecord - attr_accessible \ - :cardholders_name, # str (name associated with this card) - :email, # str (cache the email associated with this card) - :name, # str (readable card name, eg. Visa *1234) - :failure_message, # accessor for temporarily storing the stripe decline message - :status, # str - :stripe_card_token, # str - :stripe_card_id, # str - :stripe_customer_id, # str - :holder, :holder_id, :holder_type, # polymorphic cardholder association - :inactive # a card is inactive. This is currently only meaningful for nonprofit cards + #TODO + # attr_accessible \ + # :cardholders_name, # str (name associated with this card) + # :email, # str (cache the email associated with this card) + # :name, # str (readable card name, eg. Visa *1234) + # :failure_message, # accessor for temporarily storing the stripe decline message + # :status, # str + # :stripe_card_token, # str + # :stripe_card_id, # str + # :stripe_customer_id, # str + # :holder, :holder_id, :holder_type, # polymorphic cardholder association + # :inactive # a card is inactive. This is currently only meaningful for nonprofit cards attr_accessor :failure_message diff --git a/app/models/charge.rb b/app/models/charge.rb index 7b300efa..42d7161d 100644 --- a/app/models/charge.rb +++ b/app/models/charge.rb @@ -3,11 +3,12 @@ class Charge < ApplicationRecord - attr_accessible \ - :amount, - :fee, - :stripe_charge_id, - :status + #TODO + # attr_accessible \ + # :amount, + # :fee, + # :stripe_charge_id, + # :status has_one :campaign, through: :donation diff --git a/app/models/comment.rb b/app/models/comment.rb index 6f4b9e52..8efbcc7b 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,10 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Comment < ApplicationRecord - attr_accessible \ - :host_id, :host_type, #parent: Event, Campaign, nil - :profile_id, - :body + #TODO + # attr_accessible \ + # :host_id, :host_type, #parent: Event, Campaign, nil + # :profile_id, + # :body validates :profile, :presence => true validates :body, :presence => true, :length => {:maximum => 200} diff --git a/app/models/custom_field_join.rb b/app/models/custom_field_join.rb index 0d96a378..fb915709 100644 --- a/app/models/custom_field_join.rb +++ b/app/models/custom_field_join.rb @@ -1,10 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CustomFieldJoin < ApplicationRecord - attr_accessible \ - :supporter, :supporter_id, - :custom_field_master, :custom_field_master_id, - :value + #TODO + # attr_accessible \ + # :supporter, :supporter_id, + # :custom_field_master, :custom_field_master_id, + # :value validates :custom_field_master, presence: true diff --git a/app/models/dispute.rb b/app/models/dispute.rb index e8b912e4..78bd2754 100644 --- a/app/models/dispute.rb +++ b/app/models/dispute.rb @@ -3,14 +3,15 @@ class Dispute < ApplicationRecord Reasons = [:unrecognized, :duplicate, :fraudulent, :subscription_canceled, :product_unacceptable, :product_not_received, :unrecognized, :credit_not_processed, :goods_services_returned_or_refused, :goods_services_cancelled, :incorrect_account_details, :insufficient_funds, :bank_cannot_process, :debit_not_authorized, :general] - Statuses = [:needs_response, :under_review, :won, :lost, :lost_and_paid] - attr_accessible \ - :gross_amount, # int - :charge_id, :charge, - :payment_id, :payment, - :status, - :reason + Statuses = [:needs_response, :under_review, :won, :lost, :lost_and_paid] + #TODO + # attr_accessible \ + # :gross_amount, # int + # :charge_id, :charge, + # :payment_id, :payment, + # :status, + # :reason belongs_to :charge belongs_to :payment diff --git a/app/models/donation.rb b/app/models/donation.rb index 1c7053d8..b830b8b6 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,25 +1,26 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Donation < ApplicationRecord - attr_accessible \ - :date, # datetime (when this donation was made) - :amount, # int (in cents) - :recurring, # bool - :anonymous, # bool - :email, # str (cached email of the donor) - :designation, # text - :dedication, # text - :comment, # text - :origin_url, # text - :nonprofit_id, :nonprofit, - :card_id, :card, # Card with which any charges were made - :supporter_id, :supporter, - :profile_id, :profile, - :campaign_id, :campaign, - :payment_id, :payment, - :event_id, :event, - :direct_debit_detail_id, :direct_debit_detail, - :payment_provider + #TODO + # attr_accessible \ + # :date, # datetime (when this donation was made) + # :amount, # int (in cents) + # :recurring, # bool + # :anonymous, # bool + # :email, # str (cached email of the donor) + # :designation, # text + # :dedication, # text + # :comment, # text + # :origin_url, # text + # :nonprofit_id, :nonprofit, + # :card_id, :card, # Card with which any charges were made + # :supporter_id, :supporter, + # :profile_id, :profile, + # :campaign_id, :campaign, + # :payment_id, :payment, + # :event_id, :event, + # :direct_debit_detail_id, :direct_debit_detail, + # :payment_provider validates :amount, presence: true, numericality: { only_integer: true } validates :supporter, presence: true diff --git a/app/models/email_draft.rb b/app/models/email_draft.rb index cc4951b0..7de0e030 100644 --- a/app/models/email_draft.rb +++ b/app/models/email_draft.rb @@ -1,12 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailDraft < ApplicationRecord - - attr_accessible \ - :nonprofit, :nonprofit_id, - :name, - :deleted, - :value, - :created_at + #TODO + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :value, + # :created_at belongs_to :nonprofit diff --git a/app/models/email_setting.rb b/app/models/email_setting.rb index 0171f071..d81db281 100644 --- a/app/models/email_setting.rb +++ b/app/models/email_setting.rb @@ -1,14 +1,15 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailSetting < ApplicationRecord - attr_accessible \ - :user_id, :user, - :nonprofit_id, :nonprofit, - :notify_payments, - :notify_campaigns, - :notify_events, - :notify_payouts, - :notify_recurring_donations + #TODO + # attr_accessible \ + # :user_id, :user, + # :nonprofit_id, :nonprofit, + # :notify_payments, + # :notify_campaigns, + # :notify_events, + # :notify_payouts, + # :notify_recurring_donations belongs_to :nonprofit belongs_to :user diff --git a/app/models/event.rb b/app/models/event.rb index 37c631f7..0e88dc03 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,38 +1,39 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Event < ApplicationRecord - attr_accessible \ - :deleted, #bool for soft-delete - :name, # str - :tagline, # str - :summary, # text - :body, # text (html) - :end_datetime, - :start_datetime, - :latitude, # float - :longitude, # float - :location, # str - :city, # str - :state_code, # str - :address, # str - :zip_code, # str - :main_image, # str - :remove_main_image, # for carrierwave - :background_image, # str - :remove_background_image, # bool carrierwave - :published, # bool - :slug, # str - :directions, # text - :venue_name, # str - :profile_id, # creator - :ticket_levels_attributes, - :show_total_raised, # bool - :show_total_count, # bool - :hide_activity_feed, # bool - :nonprofit_id, # host - :hide_title, # bool - :organizer_email, # string - :receipt_message # text + #TODO + # attr_accessible \ + # :deleted, #bool for soft-delete + # :name, # str + # :tagline, # str + # :summary, # text + # :body, # text (html) + # :end_datetime, + # :start_datetime, + # :latitude, # float + # :longitude, # float + # :location, # str + # :city, # str + # :state_code, # str + # :address, # str + # :zip_code, # str + # :main_image, # str + # :remove_main_image, # for carrierwave + # :background_image, # str + # :remove_background_image, # bool carrierwave + # :published, # bool + # :slug, # str + # :directions, # text + # :venue_name, # str + # :profile_id, # creator + # :ticket_levels_attributes, + # :show_total_raised, # bool + # :show_total_count, # bool + # :hide_activity_feed, # bool + # :nonprofit_id, # host + # :hide_title, # bool + # :organizer_email, # string + # :receipt_message # text validates :name, :presence => true validates :end_datetime, :presence => true diff --git a/app/models/event_discount.rb b/app/models/event_discount.rb index 2888ecaa..d09d5c9b 100644 --- a/app/models/event_discount.rb +++ b/app/models/event_discount.rb @@ -1,10 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventDiscount < ApplicationRecord - attr_accessible \ - :code, - :event_id, - :name, - :percent + #TODO + # attr_accessible \ + # :code, + # :event_id, + # :name, + # :percent belongs_to :event has_many :tickets diff --git a/app/models/full_contact_info.rb b/app/models/full_contact_info.rb index 7862d89d..2035a618 100644 --- a/app/models/full_contact_info.rb +++ b/app/models/full_contact_info.rb @@ -1,19 +1,20 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactInfo < ApplicationRecord - attr_accessible \ - :email, - :full_name, - :gender, - :city, - :county, - :state_code, - :country, - :continent, - :age, - :age_range, - :location_general, - :supporter_id, :supporter, - :websites + #TODO + # attr_accessible \ + # :email, + # :full_name, + # :gender, + # :city, + # :county, + # :state_code, + # :country, + # :continent, + # :age, + # :age_range, + # :location_general, + # :supporter_id, :supporter, + # :websites has_many :full_contact_photos has_many :full_contact_social_profiles diff --git a/app/models/full_contact_org.rb b/app/models/full_contact_org.rb index 05acfc9c..2f0b0a43 100644 --- a/app/models/full_contact_org.rb +++ b/app/models/full_contact_org.rb @@ -1,15 +1,16 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactOrg < ApplicationRecord - attr_accessible \ - :name, - :is_primary, - :name, - :start_date, - :end_date, - :title, - :current, - :full_contact_info_id, :full_contact_info + #TODO + # attr_accessible \ + # :name, + # :is_primary, + # :name, + # :start_date, + # :end_date, + # :title, + # :current, + # :full_contact_info_id, :full_contact_info belongs_to :full_contact_info diff --git a/app/models/full_contact_photo.rb b/app/models/full_contact_photo.rb index b96a5bb7..181b92fe 100644 --- a/app/models/full_contact_photo.rb +++ b/app/models/full_contact_photo.rb @@ -1,11 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactPhoto < ApplicationRecord - attr_accessible \ - :full_contact_info, - :full_contact_info_id, - :type_id, # i.e. twitter, linkedin, facebook - :is_primary, #bool - :url #string + #TODO + # attr_accessible \ + # :full_contact_info, + # :full_contact_info_id, + # :type_id, # i.e. twitter, linkedin, facebook + # :is_primary, #bool + # :url #string belongs_to :full_contact_info diff --git a/app/models/full_contact_social_profile.rb b/app/models/full_contact_social_profile.rb index b98a4dd3..b85511dd 100644 --- a/app/models/full_contact_social_profile.rb +++ b/app/models/full_contact_social_profile.rb @@ -1,13 +1,14 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactSocialProfile < ApplicationRecord - attr_accessible \ - :full_contact_info, - :full_contact_info_id, - :type_id, # i.e. twitter, linkedin, facebook - :username, #string - :uid, # string - :bio, #string - :url #string + #TODO + # attr_accessible \ + # :full_contact_info, + # :full_contact_info_id, + # :type_id, # i.e. twitter, linkedin, facebook + # :username, #string + # :uid, # string + # :bio, #string + # :url #string belongs_to :full_contact_info diff --git a/app/models/full_contact_topic.rb b/app/models/full_contact_topic.rb index 0839097b..5dcdbf97 100644 --- a/app/models/full_contact_topic.rb +++ b/app/models/full_contact_topic.rb @@ -1,10 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactTopic < ApplicationRecord - attr_accessible \ - :provider, - :value, - :full_contact_info_id, :full_contact_info + #TODO + # attr_accessible \ + # :provider, + # :value, + # :full_contact_info_id, :full_contact_info belongs_to :full_contact_info diff --git a/app/models/import.rb b/app/models/import.rb index 66a06071..4c056f1e 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -1,13 +1,14 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Import < ApplicationRecord - attr_accessible \ - :user_id, :user, - :email, # email of the user who ma - :nonprofit_id, :nonprofit, - :row_count, - :imported_count, - :date + #TODO + # attr_accessible \ + # :user_id, :user, + # :email, # email of the user who ma + # :nonprofit_id, :nonprofit, + # :row_count, + # :imported_count, + # :date has_many :supporters belongs_to :nonprofit diff --git a/app/models/miscellaneous_np_info.rb b/app/models/miscellaneous_np_info.rb index a9ba9179..240d6478 100644 --- a/app/models/miscellaneous_np_info.rb +++ b/app/models/miscellaneous_np_info.rb @@ -1,9 +1,10 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class MiscellaneousNpInfo < ApplicationRecord - attr_accessible \ - :donate_again_url, - :change_amount_message + #TODO + # attr_accessible \ + # :donate_again_url, + # :change_amount_message belongs_to :nonprofit end diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index b2bda8d4..f03781f1 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -3,54 +3,55 @@ class Nonprofit < ApplicationRecord Categories = ["Public Benefit", "Human Services", "Education", "Civic Duty", "Human Rights", "Animals", "Environment", "Health", "Arts, Culture, Humanities", "International", "Children", "Religion", "LGBTQ", "Women's Rights", "Disaster Relief", "Veterans"] - attr_accessible \ - :name, # str - :stripe_account_id, # str - :summary, # text: paragraph-sized organization summary - :tagline, # str - :email, # str: public organization contact email - :phone, # str: public org contact phone - :main_image, # str: url of featured image - first image in profile carousel - :second_image, # str: url of 2nd image in carousel - :third_image, # str: url of 3rd image in carousel - :background_image, # str: url of large profile background - :remove_background_image, #bool carrierwave - :logo, # str: small logo image url for searching - :zip_code, # int - :website, # str: their own website url - :categories, # text [str]: see the constant Categories - :achievements, # text [str]: highlights about this org - :full_description, # text - :state_code, # str: two-letter state code (eg. CA) - :statement, # str: bank statement for donations towards the nonprofit - :city, # str - :slug, # str - :city_slug, #str - :state_code_slug, #str - :ein, # str: employee identification number - :published, # boolean; whether to display this profile - :vetted, # bool: Whether a super admin (one of CommitChange's employees) have approved this org - :verification_status, # str (either 'pending', 'unverified', 'escalated', 'verified' -- whether the org has submitted the identity verification form and it has been approved) - :latitude, # float: geocoder gem - :longitude, # float: geocoder gem - :timezone, # str - :address, # text - :thank_you_note, # text - :referrer, # str - :no_anon, # bool: whether to allow anonymous donations - :roles_attributes, - :brand_font, #string (lowercase key eg. 'helvetica') - :brand_color, #string (hex color value) - :hide_activity_feed, # bool - :tracking_script, - :facebook, #string (url) - :twitter, #string (url) - :youtube, #string (url) - :instagram, #string (url) - :blog, #string (url) - :card_failure_message_top, # text - :card_failure_message_bottom, # text - :autocomplete_supporter_address # boolean + #TODO + # attr_accessible \ + # :name, # str + # :stripe_account_id, # str + # :summary, # text: paragraph-sized organization summary + # :tagline, # str + # :email, # str: public organization contact email + # :phone, # str: public org contact phone + # :main_image, # str: url of featured image - first image in profile carousel + # :second_image, # str: url of 2nd image in carousel + # :third_image, # str: url of 3rd image in carousel + # :background_image, # str: url of large profile background + # :remove_background_image, #bool carrierwave + # :logo, # str: small logo image url for searching + # :zip_code, # int + # :website, # str: their own website url + # :categories, # text [str]: see the constant Categories + # :achievements, # text [str]: highlights about this org + # :full_description, # text + # :state_code, # str: two-letter state code (eg. CA) + # :statement, # str: bank statement for donations towards the nonprofit + # :city, # str + # :slug, # str + # :city_slug, #str + # :state_code_slug, #str + # :ein, # str: employee identification number + # :published, # boolean; whether to display this profile + # :vetted, # bool: Whether a super admin (one of CommitChange's employees) have approved this org + # :verification_status, # str (either 'pending', 'unverified', 'escalated', 'verified' -- whether the org has submitted the identity verification form and it has been approved) + # :latitude, # float: geocoder gem + # :longitude, # float: geocoder gem + # :timezone, # str + # :address, # text + # :thank_you_note, # text + # :referrer, # str + # :no_anon, # bool: whether to allow anonymous donations + # :roles_attributes, + # :brand_font, #string (lowercase key eg. 'helvetica') + # :brand_color, #string (hex color value) + # :hide_activity_feed, # bool + # :tracking_script, + # :facebook, #string (url) + # :twitter, #string (url) + # :youtube, #string (url) + # :instagram, #string (url) + # :blog, #string (url) + # :card_failure_message_top, # text + # :card_failure_message_bottom, # text + # :autocomplete_supporter_address # boolean has_many :payouts has_many :charges diff --git a/app/models/nonprofit_account.rb b/app/models/nonprofit_account.rb index b5ee62d0..b4f67ffe 100644 --- a/app/models/nonprofit_account.rb +++ b/app/models/nonprofit_account.rb @@ -1,9 +1,10 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitAccount < ApplicationRecord - attr_accessible \ - :stripe_account_id, #str - :nonprofit, :nonprofit_id #int + #TODO + # attr_accessible \ + # :stripe_account_id, #str + # :nonprofit, :nonprofit_id #int belongs_to :nonprofit diff --git a/app/models/payment.rb b/app/models/payment.rb index 4e622d7b..be3d68e8 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -5,13 +5,14 @@ class Payment < ApplicationRecord - attr_accessible \ - :towards, - :gross_amount, - :refund_total, - :fee_total, - :kind, - :date +#TODO +# attr_accessible \ +# :towards, +# :gross_amount, +# :refund_total, +# :fee_total, +# :kind, +# :date belongs_to :supporter belongs_to :nonprofit diff --git a/app/models/payment_payout.rb b/app/models/payment_payout.rb index 3c686763..b3b01465 100644 --- a/app/models/payment_payout.rb +++ b/app/models/payment_payout.rb @@ -13,11 +13,12 @@ class PaymentPayout < ApplicationRecord - attr_accessible \ - :payment_id, :payment, - :charge_id, :charge, # deprecated - :payout_id, :payout, - :total_fees # int (cents) + #TODO + # attr_accessible \ + # :payment_id, :payment, + # :charge_id, :charge, # deprecated + # :payout_id, :payout, + # :total_fees # int (cents) belongs_to :charge # deprecated belongs_to :payment diff --git a/app/models/payout.rb b/app/models/payout.rb index 58fd8542..6529ea1d 100644 --- a/app/models/payout.rb +++ b/app/models/payout.rb @@ -6,20 +6,21 @@ class Payout < ApplicationRecord - attr_accessible \ - :scheduled, # bool (whether this was made automatically at the beginning of the month) - :count, # int (number of donations for this payout) - :ach_fee, # int (in cents, the total fee for the payout itself) - :gross_amount, # int (in cents, total amount before fees) - :fee_total, # int (in cents, total amount of fees) - :net_amount, # int (in cents, total amount after fees for this payout) - :email, # str (cache of user email who issued this) - :user_ip, # str (ip address of the user who made this payout) - :status, # str ('pending', 'paid', 'canceled', or 'failed') - :failure_message, # str - :bank_name, # str: cache of the nonprofit's bank name - :stripe_transfer_id, # str - :nonprofit_id, :nonprofit + #TODO + # attr_accessible \ + # :scheduled, # bool (whether this was made automatically at the beginning of the month) + # :count, # int (number of donations for this payout) + # :ach_fee, # int (in cents, the total fee for the payout itself) + # :gross_amount, # int (in cents, total amount before fees) + # :fee_total, # int (in cents, total amount of fees) + # :net_amount, # int (in cents, total amount after fees for this payout) + # :email, # str (cache of user email who issued this) + # :user_ip, # str (ip address of the user who made this payout) + # :status, # str ('pending', 'paid', 'canceled', or 'failed') + # :failure_message, # str + # :bank_name, # str: cache of the nonprofit's bank name + # :stripe_transfer_id, # str + # :nonprofit_id, :nonprofit belongs_to :nonprofit has_one :bank_account, through: :nonprofit diff --git a/app/models/profile.rb b/app/models/profile.rb index d9e17c0d..be081dd6 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,23 +1,24 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Profile < ApplicationRecord - attr_accessible \ - :registered, # bool - :mini_bio, - :first_name, # str - :last_name, # str - :name, - :phone, # str - :address, # str - :email, # str - :city, # str - :state_code, # str (eg. CA) - :zip_code, # str - :privacy_settings, # text [str]: XXX deprecated - :picture, # str: either their social network pic or a stored pic on S3 - :anonymous, # bool: negates all privacy_settings - :city_state, - :user_id + #TODO + # attr_accessible \ + # :registered, # bool + # :mini_bio, + # :first_name, # str + # :last_name, # str + # :name, + # :phone, # str + # :address, # str + # :email, # str + # :city, # str + # :state_code, # str (eg. CA) + # :zip_code, # str + # :privacy_settings, # text [str]: XXX deprecated + # :picture, # str: either their social network pic or a stored pic on S3 + # :anonymous, # bool: negates all privacy_settings + # :city_state, + # :user_id validates :email, format: {with: Email::Regex}, allow_blank: true diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index 3e018bbe..55e585fd 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,21 +1,22 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RecurringDonation < ApplicationRecord - attr_accessible \ - :amount, # int (cents) - :active, # bool (whether this recurring donation should still be paid) - :paydate, # int (fixed date of the month for monthly recurring donations) - :interval, # int (interval of time, ie the '3' in '3 months') - :time_unit, # str ('month', 'day', 'week', or 'year') - :start_date, # date (when to start this recurring donation) - :end_date, # date (when to deactivate this recurring donation) - :n_failures, # int (how many times the charge has failed) - :edit_token, # str / uuid to validate the editing page, linked from their email client - :cancelled_by, # str email of user/supporter who made the cancellation - :cancelled_at, # datetime of user/supporter who made the cancellation - :donation_id, :donation, - :nonprofit_id, :nonprofit, - :supporter_id #used because things are messed up in the datamodel + #TODO: + # attr_accessible \ + # :amount, # int (cents) + # :active, # bool (whether this recurring donation should still be paid) + # :paydate, # int (fixed date of the month for monthly recurring donations) + # :interval, # int (interval of time, ie the '3' in '3 months') + # :time_unit, # str ('month', 'day', 'week', or 'year') + # :start_date, # date (when to start this recurring donation) + # :end_date, # date (when to deactivate this recurring donation) + # :n_failures, # int (how many times the charge has failed) + # :edit_token, # str / uuid to validate the editing page, linked from their email client + # :cancelled_by, # str email of user/supporter who made the cancellation + # :cancelled_at, # datetime of user/supporter who made the cancellation + # :donation_id, :donation, + # :nonprofit_id, :nonprofit, + # :supporter_id #used because things are messed up in the datamodel scope :active, -> {where(active: true)} scope :inactive, -> {where(active: [false,nil])} diff --git a/app/models/refund.rb b/app/models/refund.rb index 25e7cf8b..ef37db60 100644 --- a/app/models/refund.rb +++ b/app/models/refund.rb @@ -3,16 +3,17 @@ class Refund < ApplicationRecord Reasons = [:duplicate, :fraudulent, :requested_by_customer] - attr_accessible \ - :amount, # int - :comment, # text - :reason, # str ('duplicate', 'fraudulent', or 'requested_by_customer') - :stripe_refund_id, - :disbursed, # boolean (whether this refund has been counted in a payout) - :failure_message, # str (accessor for storing the Stripe error message) - :user_id, :user, # user who made this refund - :payment_id, :payment, # negative payment that records this refund - :charge_id, :charge + # TODO: + # attr_accessible \ + # :amount, # int + # :comment, # text + # :reason, # str ('duplicate', 'fraudulent', or 'requested_by_customer') + # :stripe_refund_id, + # :disbursed, # boolean (whether this refund has been counted in a payout) + # :failure_message, # str (accessor for storing the Stripe error message) + # :user_id, :user, # user who made this refund + # :payment_id, :payment, # negative payment that records this refund + # :charge_id, :charge attr_accessor :failure_message diff --git a/app/models/role.rb b/app/models/role.rb index 8bbd5263..215e24af 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -10,10 +10,11 @@ class Role < ApplicationRecord :event_editor # // ] - attr_accessible \ - :name, - :user_id, :user, - :host, :host_id, :host_type # nil, "Nonprofit", "Campaign", "Event" + # TODO: + # attr_accessible \ + # :name, + # :user_id, :user, + # :host, :host_id, :host_type # nil, "Nonprofit", "Campaign", "Event" belongs_to :user belongs_to :host, polymorphic: true diff --git a/app/models/supporter.rb b/app/models/supporter.rb index 05674b16..cdb087a2 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -1,33 +1,34 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Supporter < ApplicationRecord - attr_accessible \ - :search_vectors, - :profile_id, :profile, - :nonprofit_id, :nonprofit, - :full_contact_info, :full_contact_info_id, - :import_id, :import, - :name, - :first_name, - :last_name, - :email, - :address, - :city, - :state_code, - :country, - :phone, - :organization, - :latitude, - :locale, - :longitude, - :zip_code, - :total_raised, - :notes, - :fields, - :anonymous, - :deleted, # bool (flag for soft delete) - :email_unsubscribe_uuid, #string - :is_unsubscribed_from_emails #bool + #TODO + # attr_accessible \ + # :search_vectors, + # :profile_id, :profile, + # :nonprofit_id, :nonprofit, + # :full_contact_info, :full_contact_info_id, + # :import_id, :import, + # :name, + # :first_name, + # :last_name, + # :email, + # :address, + # :city, + # :state_code, + # :country, + # :phone, + # :organization, + # :latitude, + # :locale, + # :longitude, + # :zip_code, + # :total_raised, + # :notes, + # :fields, + # :anonymous, + # :deleted, # bool (flag for soft delete) + # :email_unsubscribe_uuid, #string + # :is_unsubscribed_from_emails #bool belongs_to :profile belongs_to :nonprofit diff --git a/app/models/supporter_email.rb b/app/models/supporter_email.rb index 0e9b2e53..3cd856f9 100644 --- a/app/models/supporter_email.rb +++ b/app/models/supporter_email.rb @@ -1,14 +1,15 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SupporterEmail < ApplicationRecord - attr_accessible \ - :to, - :from, - :subject, - :body, - :recipient_count, - :supporter_id, :supporter, - :nonprofit_id, - :gmail_thread_id + # TODO + # attr_accessible \ + # :to, + # :from, + # :subject, + # :body, + # :recipient_count, + # :supporter_id, :supporter, + # :nonprofit_id, + # :gmail_thread_id belongs_to :supporter validates_presence_of :nonprofit_id diff --git a/app/models/supporter_note.rb b/app/models/supporter_note.rb index ff7d29f9..8701dd2b 100644 --- a/app/models/supporter_note.rb +++ b/app/models/supporter_note.rb @@ -1,9 +1,10 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SupporterNote < ApplicationRecord - attr_accessible \ - :content, - :supporter_id, :supporter + #TODO + # attr_accessible \ + # :content, + # :supporter_id, :supporter belongs_to :supporter has_many :activities, as: :attachment, dependent: :destroy diff --git a/app/models/tag_join.rb b/app/models/tag_join.rb index d531ddb9..45ee10c5 100644 --- a/app/models/tag_join.rb +++ b/app/models/tag_join.rb @@ -1,9 +1,10 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TagJoin < ApplicationRecord - attr_accessible \ - :supporter, :supporter_id, - :tag_master, :tag_master_id + #TODO + # attr_accessible \ + # :supporter, :supporter_id, + # :tag_master, :tag_master_id validates :tag_master, presence: true diff --git a/app/models/tag_master.rb b/app/models/tag_master.rb index 836bdbfb..9bac71ce 100644 --- a/app/models/tag_master.rb +++ b/app/models/tag_master.rb @@ -1,11 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TagMaster < ApplicationRecord - attr_accessible \ - :nonprofit, :nonprofit_id, - :name, - :deleted, - :created_at + #TODO: + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :created_at validates :name, presence: true validate :no_dupes, on: :create diff --git a/app/models/ticket_level.rb b/app/models/ticket_level.rb index 130157e0..d9d7942d 100644 --- a/app/models/ticket_level.rb +++ b/app/models/ticket_level.rb @@ -1,17 +1,18 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketLevel < ApplicationRecord - attr_accessible \ - :amount, #integer - :amount_dollars, #accessor, string - :name, #string - :description, #text - :quantity, #integer - :deleted, #bool for soft delete - :event_id, - :admin_only, #bool, only admins can create tickets for this level - :limit, #int: for limiting the number of tickets to be sold - :order #int: order in which to be displayed + #TODO + # attr_accessible \ + # :amount, #integer + # :amount_dollars, #accessor, string + # :name, #string + # :description, #text + # :quantity, #integer + # :deleted, #bool for soft delete + # :event_id, + # :admin_only, #bool, only admins can create tickets for this level + # :limit, #int: for limiting the number of tickets to be sold + # :order #int: order in which to be displayed attr_accessor :amount_dollars diff --git a/app/models/user.rb b/app/models/user.rb index dffee4cb..31b0aba1 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,29 +1,30 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class User < ApplicationRecord - attr_accessible \ - :email, # str: balidated with Devise - :password, # str: hashed with bcrypt - :phone, # str - :location, - :city, - :state_code, - :password_confirmation, # accessor: used on registration - :remember_me, # bool: don't sign user out for a while - :provider, # str: OAuth provider - :uid, # str: OAuth user ID - :pending_password, # bool: User registered with oauth and did not set a password - :name, # str: created with oauth - :auto_generated, # bool: flag whether a password was auto-generated for this account - :referer, # str: ID of the user who referred this account - :latitude, - :longitude, - :reset_password_token, - :reset_password_sent_at, - :picture, # str: url for fb or twitter pic - :current_password, # accessor: for updating pass - :profile_attributes, - :phone + #TODO: + # attr_accessible \ + # :email, # str: balidated with Devise + # :password, # str: hashed with bcrypt + # :phone, # str + # :location, + # :city, + # :state_code, + # :password_confirmation, # accessor: used on registration + # :remember_me, # bool: don't sign user out for a while + # :provider, # str: OAuth provider + # :uid, # str: OAuth user ID + # :pending_password, # bool: User registered with oauth and did not set a password + # :name, # str: created with oauth + # :auto_generated, # bool: flag whether a password was auto-generated for this account + # :referer, # str: ID of the user who referred this account + # :latitude, + # :longitude, + # :reset_password_token, + # :reset_password_sent_at, + # :picture, # str: url for fb or twitter pic + # :current_password, # accessor: for updating pass + # :profile_attributes, + # :phone geocoded_by :location From 8db525c2b85a286448bc8269550bc6c18a638eea Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 21 Jun 2019 17:14:27 -0500 Subject: [PATCH 038/440] Comment out mass assignment sanitizer --- Gemfile.lock | 8 -------- config/environments/development.rb | 2 +- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 5a900acc..7620cb74 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -72,10 +72,6 @@ GEM tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) - airbrake (8.0.1) - airbrake-ruby (~> 3.0) - airbrake-ruby (3.1.0) - tdigest (= 0.1.1) amq-protocol (2.3.0) andand (1.3.3) arel (7.1.4) @@ -361,7 +357,6 @@ GEM thor (>= 0.18.1, < 2.0) rainbow (3.0.0) rake (12.3.2) - rbtree (0.4.2) request_store (1.4.1) rack (>= 1.4) require_all (2.0.0) @@ -448,8 +443,6 @@ GEM stripe (1.58.0) rest-client (>= 1.4, < 4.0) table_print (1.5.6) - tdigest (0.1.1) - rbtree (~> 0.4.2) test-unit (3.2.8) power_assert thor (0.19.4) @@ -489,7 +482,6 @@ PLATFORMS DEPENDENCIES action_mailer_matchers - airbrake (~> 8.0.1) aws-sdk (~> 1) aws-ses binding_of_caller diff --git a/config/environments/development.rb b/config/environments/development.rb index ab9d88d3..8c42eb73 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -52,7 +52,7 @@ Rails.application.configure do config.active_support.deprecation = :log # Raise exception on mass assignment protection for Active Record models - config.active_record.mass_assignment_sanitizer = :strict + # config.active_record.mass_assignment_sanitizer = :strict # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load From 28df345478b82fc0c0670c7a9d59c2efab03e6c0 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Fri, 21 Jun 2019 17:15:07 -0500 Subject: [PATCH 039/440] Comment out quiet_assets --- config/initializers/quiet_assets.rb | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/config/initializers/quiet_assets.rb b/config/initializers/quiet_assets.rb index 98f72a62..7bb65cb7 100644 --- a/config/initializers/quiet_assets.rb +++ b/config/initializers/quiet_assets.rb @@ -1,14 +1,14 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later if Rails.env.development? - Rails.application.assets.logger = Logger.new('/dev/null') - Rails::Rack::Logger.class_eval do - def call_with_quiet_assets(env) - previous_level = Rails.logger.level - Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/assets/} - call_without_quiet_assets(env) - ensure - Rails.logger.level = previous_level - end - alias_method_chain :call, :quiet_assets - end + # Rails.application.assets.logger = Logger.new('/dev/null') + # Rails::Rack::Logger.class_eval do + # def call_with_quiet_assets(env) + # previous_level = Rails.logger.level + # Rails.logger.level = Logger::ERROR if env['PATH_INFO'] =~ %r{^/assets/} + # call_without_quiet_assets(env) + # ensure + # Rails.logger.level = previous_level + # end + # alias_method_chain :call, :quiet_assets + # end end From d9895890a0aff13185aa2df7ecd40e87661cdf11 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 09:50:34 +0200 Subject: [PATCH 040/440] chore: create uniq db for continous integration --- config/database.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/database.yml b/config/database.yml index 50c799fe..a36d1ef2 100755 --- a/config/database.yml +++ b/config/database.yml @@ -33,7 +33,7 @@ test: ci: adapter: postgresql encoding: unicode - database: commitchange_development + database: commitchange_ci pool: 5 username: admin password: password From 34b4604c7a7a18a7a6f12f825c8fd8695d416668 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 09:51:00 +0200 Subject: [PATCH 041/440] fix: Puma server config syntax --- config/puma.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/puma.rb b/config/puma.rb index 5695f3af..b93dad33 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -13,9 +13,9 @@ preload_app! if ENV['RAILS_ENV'] != 'development' rackup DefaultRackup port ENV.fetch("PORT") { 5000 } -environment ENV.fetch('RAILS_ENV'{ 'development' } +environment ENV.fetch('RAILS_ENV'){ 'development' } -workers ENV['WEB_CONCURRENCY'].fetch { 1 } +workers Integer(ENV['WEB_CONCURRENCY'] || 1) From dd64ee5159931516b9e6a1be9169a49e98c67dfb Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 09:53:59 +0200 Subject: [PATCH 042/440] feat: replace deprecated before_filter with before_action --- app/controllers/activities_controller.rb | 2 +- app/controllers/application_controller.rb | 2 +- app/controllers/aws_presigned_posts_controller.rb | 2 +- app/controllers/billing_subscriptions_controller.rb | 2 +- app/controllers/campaign_gift_options_controller.rb | 2 +- .../campaigns/campaign_gift_options_controller.rb | 2 +- app/controllers/campaigns/donations_controller.rb | 2 +- app/controllers/campaigns/supporters_controller.rb | 2 +- app/controllers/campaigns_controller.rb | 6 +++--- app/controllers/cards_controller.rb | 2 +- app/controllers/email_settings_controller.rb | 2 +- app/controllers/emails_controller.rb | 2 +- app/controllers/event_discounts_controller.rb | 2 +- app/controllers/events_controller.rb | 4 ++-- app/controllers/image_attachments_controller.rb | 2 +- app/controllers/maps_controller.rb | 4 ++-- app/controllers/nonprofits/activities_controller.rb | 2 +- app/controllers/nonprofits/bank_accounts_controller.rb | 2 +- app/controllers/nonprofits/button_controller.rb | 2 +- app/controllers/nonprofits/cards_controller.rb | 2 +- app/controllers/nonprofits/charges_controller.rb | 2 +- app/controllers/nonprofits/custom_field_joins_controller.rb | 2 +- .../nonprofits/custom_field_masters_controller.rb | 2 +- app/controllers/nonprofits/donations_controller.rb | 4 ++-- app/controllers/nonprofits/email_lists_controller.rb | 2 +- app/controllers/nonprofits/imports_controller.rb | 2 +- .../nonprofits/miscellaneous_np_infos_controller.rb | 2 +- app/controllers/nonprofits/nonprofit_keys_controller.rb | 2 +- app/controllers/nonprofits/payments_controller.rb | 2 +- app/controllers/nonprofits/payouts_controller.rb | 4 ++-- .../nonprofits/recurring_donations_controller.rb | 2 +- app/controllers/nonprofits/refunds_controller.rb | 2 +- app/controllers/nonprofits/reports_controller.rb | 2 +- app/controllers/nonprofits/supporter_emails_controller.rb | 2 +- app/controllers/nonprofits/supporter_notes_controller.rb | 2 +- app/controllers/nonprofits/supporters_controller.rb | 4 ++-- app/controllers/nonprofits/tag_joins_controller.rb | 2 +- app/controllers/nonprofits/tag_masters_controller.rb | 2 +- app/controllers/nonprofits_controller.rb | 4 ++-- app/controllers/profiles_controller.rb | 2 +- app/controllers/roles_controller.rb | 2 +- app/controllers/settings_controller.rb | 2 +- app/controllers/super_admins_controller.rb | 2 +- app/controllers/ticket_levels_controller.rb | 2 +- app/controllers/tickets_controller.rb | 4 ++-- 45 files changed, 54 insertions(+), 54 deletions(-) diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index 07dd5f2c..c2028140 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ActivitiesController < ApplicationController - before_filter :authenticate_user!, only: [:create] + before_action :authenticate_user!, only: [:create] def create json_saved Activity.create(params[:activity]) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 76f75165..09be7bba 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ApplicationController < ActionController::Base - before_filter :set_locale, :redirect_to_maintenance + before_action :set_locale, :redirect_to_maintenance protect_from_forgery diff --git a/app/controllers/aws_presigned_posts_controller.rb b/app/controllers/aws_presigned_posts_controller.rb index bdea0839..a6855266 100644 --- a/app/controllers/aws_presigned_posts_controller.rb +++ b/app/controllers/aws_presigned_posts_controller.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AwsPresignedPostsController < ApplicationController - before_filter :authenticate_user! + before_action :authenticate_user! # post /presigned_posts # Create some keys using the AWS gem so the user can do direct-to-S3 uploads diff --git a/app/controllers/billing_subscriptions_controller.rb b/app/controllers/billing_subscriptions_controller.rb index 1f63fb4b..cee634ed 100644 --- a/app/controllers/billing_subscriptions_controller.rb +++ b/app/controllers/billing_subscriptions_controller.rb @@ -2,7 +2,7 @@ class BillingSubscriptionsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! def create_trial render JsonResp.new(params){|params| diff --git a/app/controllers/campaign_gift_options_controller.rb b/app/controllers/campaign_gift_options_controller.rb index 47b21367..044aab0b 100644 --- a/app/controllers/campaign_gift_options_controller.rb +++ b/app/controllers/campaign_gift_options_controller.rb @@ -2,7 +2,7 @@ class CampaignGiftOptionsController < ApplicationController include Controllers::CampaignHelper - before_filter :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order] + before_action :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order] def index @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') diff --git a/app/controllers/campaigns/campaign_gift_options_controller.rb b/app/controllers/campaigns/campaign_gift_options_controller.rb index 83e5da31..eba8451d 100644 --- a/app/controllers/campaigns/campaign_gift_options_controller.rb +++ b/app/controllers/campaigns/campaign_gift_options_controller.rb @@ -2,7 +2,7 @@ module Campaigns; class CampaignGiftOptionsController < ApplicationController include Controllers::CampaignHelper - before_filter :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order, :report] + before_action :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order, :report] def report respond_to do |format| diff --git a/app/controllers/campaigns/donations_controller.rb b/app/controllers/campaigns/donations_controller.rb index 44cb27a1..a53c0f3d 100644 --- a/app/controllers/campaigns/donations_controller.rb +++ b/app/controllers/campaigns/donations_controller.rb @@ -3,7 +3,7 @@ module Campaigns class DonationsController < ApplicationController include Controllers::CampaignHelper - before_filter :authenticate_campaign_editor!, only: [:index] + before_action :authenticate_campaign_editor!, only: [:index] def index respond_to do |format| diff --git a/app/controllers/campaigns/supporters_controller.rb b/app/controllers/campaigns/supporters_controller.rb index 9796e687..5f77ffd0 100644 --- a/app/controllers/campaigns/supporters_controller.rb +++ b/app/controllers/campaigns/supporters_controller.rb @@ -3,7 +3,7 @@ module Campaigns class SupportersController < ApplicationController include Controllers::CampaignHelper - before_filter :authenticate_campaign_editor!, only: [:index] + before_action :authenticate_campaign_editor!, only: [:index] def index @panels_layout = true diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index 03e95d51..a6d34d21 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -3,9 +3,9 @@ class CampaignsController < ApplicationController include Controllers::CampaignHelper helper_method :current_campaign_editor? - before_filter :authenticate_confirmed_user!, only: [:create, :name_and_id, :duplicate] - before_filter :authenticate_campaign_editor!, only: [:update, :soft_delete] - before_filter :check_nonprofit_status, only: [:index, :show] + before_action :authenticate_confirmed_user!, only: [:create, :name_and_id, :duplicate] + before_action :authenticate_campaign_editor!, only: [:update, :soft_delete] + before_action :check_nonprofit_status, only: [:index, :show] def index @nonprofit = current_nonprofit diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 4e84fd1e..bcd7c72b 100755 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CardsController < ApplicationController - before_filter :authenticate_user!, :except => [:create] + before_action :authenticate_user!, :except => [:create] # post /cards def create diff --git a/app/controllers/email_settings_controller.rb b/app/controllers/email_settings_controller.rb index 5eaf43fa..1c94c8b2 100644 --- a/app/controllers/email_settings_controller.rb +++ b/app/controllers/email_settings_controller.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailSettingsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index user = current_role?(:super_admin) ? User.find(params[:user_id]) : current_user diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb index 7ece89da..60c2b324 100644 --- a/app/controllers/emails_controller.rb +++ b/app/controllers/emails_controller.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailsController < ApplicationController - before_filter :authenticate_user! + before_action :authenticate_user! def create email = params[:email] diff --git a/app/controllers/event_discounts_controller.rb b/app/controllers/event_discounts_controller.rb index 21a4bbf5..167637c2 100644 --- a/app/controllers/event_discounts_controller.rb +++ b/app/controllers/event_discounts_controller.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventDiscountsController < ApplicationController include Controllers::EventHelper - before_filter :authenticate_event_editor!, :except => [:index] + before_action :authenticate_event_editor!, :except => [:index] def create params[:event_discount][:event_id] = current_event.id diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 6710adca..b0c82467 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -3,8 +3,8 @@ class EventsController < ApplicationController include Controllers::EventHelper helper_method :current_event_editor? - before_filter :authenticate_nonprofit_user!, only: :name_and_id - before_filter :authenticate_event_editor!, only: [:update, :soft_delete, :stats, :create, :duplicate] + before_action :authenticate_nonprofit_user!, only: :name_and_id + before_action :authenticate_event_editor!, only: [:update, :soft_delete, :stats, :create, :duplicate] def index diff --git a/app/controllers/image_attachments_controller.rb b/app/controllers/image_attachments_controller.rb index aa378f5d..9f47cbbd 100644 --- a/app/controllers/image_attachments_controller.rb +++ b/app/controllers/image_attachments_controller.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ImageAttachmentsController < ApplicationController - before_filter :authenticate_confirmed_user! + before_action :authenticate_confirmed_user! def create # must return json with a link attr # http://editor.froala.com/server-integrations/php-image-upload diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 8fe38819..1e5df927 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -2,8 +2,8 @@ class MapsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_super_associate!, only: :all_supporters - before_filter :authenticate_nonprofit_user!, only: [:all_npo_supporters, :specific_npo_supporters] + before_action :authenticate_super_associate!, only: :all_supporters + before_action :authenticate_nonprofit_user!, only: [:all_npo_supporters, :specific_npo_supporters] # used on admin/nonprofits_map and front page def all_npos diff --git a/app/controllers/nonprofits/activities_controller.rb b/app/controllers/nonprofits/activities_controller.rb index 6824e8a7..87696c3a 100644 --- a/app/controllers/nonprofits/activities_controller.rb +++ b/app/controllers/nonprofits/activities_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class ActivitiesController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! # get /nonprofits/:nonprofit_id/supporters/:supporter_id/activities def index diff --git a/app/controllers/nonprofits/bank_accounts_controller.rb b/app/controllers/nonprofits/bank_accounts_controller.rb index c68ef006..9073c33d 100644 --- a/app/controllers/nonprofits/bank_accounts_controller.rb +++ b/app/controllers/nonprofits/bank_accounts_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class BankAccountsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! # post /nonprofits/:nonprofit_id/bank_account # must pass in the user's password as params[:password] diff --git a/app/controllers/nonprofits/button_controller.rb b/app/controllers/nonprofits/button_controller.rb index 83a6765e..a2258b36 100644 --- a/app/controllers/nonprofits/button_controller.rb +++ b/app/controllers/nonprofits/button_controller.rb @@ -4,7 +4,7 @@ class ButtonController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_user! + before_action :authenticate_user! def send_code diff --git a/app/controllers/nonprofits/cards_controller.rb b/app/controllers/nonprofits/cards_controller.rb index fc0e377b..1ee6fdee 100644 --- a/app/controllers/nonprofits/cards_controller.rb +++ b/app/controllers/nonprofits/cards_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class CardsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def edit @nonprofit = current_nonprofit diff --git a/app/controllers/nonprofits/charges_controller.rb b/app/controllers/nonprofits/charges_controller.rb index e3d87a6c..25ee872d 100644 --- a/app/controllers/nonprofits/charges_controller.rb +++ b/app/controllers/nonprofits/charges_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class ChargesController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, only: :index + before_action :authenticate_nonprofit_user!, only: :index # get /nonprofit/:nonprofit_id/charges def index diff --git a/app/controllers/nonprofits/custom_field_joins_controller.rb b/app/controllers/nonprofits/custom_field_joins_controller.rb index 9c1a7fe2..2c36cfaa 100644 --- a/app/controllers/nonprofits/custom_field_joins_controller.rb +++ b/app/controllers/nonprofits/custom_field_joins_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class CustomFieldJoinsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index @custom_field_joins = current_nonprofit diff --git a/app/controllers/nonprofits/custom_field_masters_controller.rb b/app/controllers/nonprofits/custom_field_masters_controller.rb index f4e75a14..17fad57b 100644 --- a/app/controllers/nonprofits/custom_field_masters_controller.rb +++ b/app/controllers/nonprofits/custom_field_masters_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class CustomFieldMastersController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index @custom_field_masters = current_nonprofit diff --git a/app/controllers/nonprofits/donations_controller.rb b/app/controllers/nonprofits/donations_controller.rb index 4599788b..80cd72f0 100644 --- a/app/controllers/nonprofits/donations_controller.rb +++ b/app/controllers/nonprofits/donations_controller.rb @@ -3,8 +3,8 @@ module Nonprofits class DonationsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, only: [:index, :update] - before_filter :authenticate_campaign_editor!, only: [:create_offsite] + before_action :authenticate_nonprofit_user!, only: [:index, :update] + before_action :authenticate_campaign_editor!, only: [:create_offsite] # get /nonprofit/:nonprofit_id/donations def index diff --git a/app/controllers/nonprofits/email_lists_controller.rb b/app/controllers/nonprofits/email_lists_controller.rb index 1f3a5af0..25a0d8fb 100644 --- a/app/controllers/nonprofits/email_lists_controller.rb +++ b/app/controllers/nonprofits/email_lists_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class EmailListsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index render_json{ Qx.fetch(:email_lists, nonprofit_id: params[:nonprofit_id]) } diff --git a/app/controllers/nonprofits/imports_controller.rb b/app/controllers/nonprofits/imports_controller.rb index b942bfc0..9498680a 100644 --- a/app/controllers/nonprofits/imports_controller.rb +++ b/app/controllers/nonprofits/imports_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class ImportsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! # post /nonprofits/:nonprofit_id/imports def create render_json{ diff --git a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb index e0e7df41..639c4bc0 100644 --- a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb +++ b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb @@ -4,7 +4,7 @@ module Nonprofits include Controllers::NonprofitHelper helper_method :current_nonprofit_user? - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def show respond_to do |format| diff --git a/app/controllers/nonprofits/nonprofit_keys_controller.rb b/app/controllers/nonprofits/nonprofit_keys_controller.rb index efde5950..7a247fe5 100644 --- a/app/controllers/nonprofits/nonprofit_keys_controller.rb +++ b/app/controllers/nonprofits/nonprofit_keys_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class NonprofitKeysController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! # get /nonprofits/:nonprofit_id/nonprofit_keys # pass in the :select query param, which is the name of the column of the specific token you want diff --git a/app/controllers/nonprofits/payments_controller.rb b/app/controllers/nonprofits/payments_controller.rb index 62a96bc5..f54701cc 100644 --- a/app/controllers/nonprofits/payments_controller.rb +++ b/app/controllers/nonprofits/payments_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class PaymentsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! # get /nonprofit/:nonprofit_id/payments diff --git a/app/controllers/nonprofits/payouts_controller.rb b/app/controllers/nonprofits/payouts_controller.rb index 5b0e0b55..b9bbff0f 100644 --- a/app/controllers/nonprofits/payouts_controller.rb +++ b/app/controllers/nonprofits/payouts_controller.rb @@ -3,8 +3,8 @@ module Nonprofits class PayoutsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_admin!, only: :create - before_filter :authenticate_nonprofit_user!, only: [:index, :show] + before_action :authenticate_nonprofit_admin!, only: :create + before_action :authenticate_nonprofit_user!, only: [:index, :show] def create payout = InsertPayout.with_stripe(current_nonprofit.id, { diff --git a/app/controllers/nonprofits/recurring_donations_controller.rb b/app/controllers/nonprofits/recurring_donations_controller.rb index 9aa4260e..c35ea796 100644 --- a/app/controllers/nonprofits/recurring_donations_controller.rb +++ b/app/controllers/nonprofits/recurring_donations_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class RecurringDonationsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] # get /nonprofits/:nonprofit_id/recurring_donations def index diff --git a/app/controllers/nonprofits/refunds_controller.rb b/app/controllers/nonprofits/refunds_controller.rb index aecd77a5..a06f1290 100644 --- a/app/controllers/nonprofits/refunds_controller.rb +++ b/app/controllers/nonprofits/refunds_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class RefundsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! # post /charges/:charge_id/refunds def create diff --git a/app/controllers/nonprofits/reports_controller.rb b/app/controllers/nonprofits/reports_controller.rb index fd7e0346..9dd6ca5c 100644 --- a/app/controllers/nonprofits/reports_controller.rb +++ b/app/controllers/nonprofits/reports_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class ReportsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def end_of_year respond_to do |format| diff --git a/app/controllers/nonprofits/supporter_emails_controller.rb b/app/controllers/nonprofits/supporter_emails_controller.rb index 724e53be..fcd39683 100644 --- a/app/controllers/nonprofits/supporter_emails_controller.rb +++ b/app/controllers/nonprofits/supporter_emails_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class SupporterEmailsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def create if params[:selecting_all] diff --git a/app/controllers/nonprofits/supporter_notes_controller.rb b/app/controllers/nonprofits/supporter_notes_controller.rb index 554e4827..3073033b 100644 --- a/app/controllers/nonprofits/supporter_notes_controller.rb +++ b/app/controllers/nonprofits/supporter_notes_controller.rb @@ -3,7 +3,7 @@ module Nonprofits class SupporterNotesController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] # post /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes def create diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index 380564cc..a81fbe9a 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -3,8 +3,8 @@ module Nonprofits class SupportersController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, except: [:new, :create] - #before_filter(except: [:create, :mailchimp_landing]){authenticate_min_nonprofit_plan(2)} + before_action :authenticate_nonprofit_user!, except: [:new, :create] + #before_action(except: [:create, :mailchimp_landing]){authenticate_min_nonprofit_plan(2)} # get /nonprofit/:nonprofit_id/supporters def index diff --git a/app/controllers/nonprofits/tag_joins_controller.rb b/app/controllers/nonprofits/tag_joins_controller.rb index 2552120c..f499f65c 100644 --- a/app/controllers/nonprofits/tag_joins_controller.rb +++ b/app/controllers/nonprofits/tag_joins_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class TagJoinsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index render_json do diff --git a/app/controllers/nonprofits/tag_masters_controller.rb b/app/controllers/nonprofits/tag_masters_controller.rb index cd0132da..6abc8a7a 100644 --- a/app/controllers/nonprofits/tag_masters_controller.rb +++ b/app/controllers/nonprofits/tag_masters_controller.rb @@ -2,7 +2,7 @@ module Nonprofits class TagMastersController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def index render json: {data: diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index d33e46a4..93893eb3 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -3,8 +3,8 @@ include Controllers::NonprofitHelper helper_method :current_nonprofit_user? - before_filter :authenticate_nonprofit_user!, only: [:dashboard, :dashboard_metrics, :dashboard_todos, :payment_history, :profile_todos, :recurring_donation_stats, :update, :verify_identity] - before_filter :authenticate_super_admin!, only: [:destroy] + before_action :authenticate_nonprofit_user!, only: [:dashboard, :dashboard_metrics, :dashboard_todos, :payment_history, :profile_todos, :recurring_donation_stats, :update, :verify_identity] + before_action :authenticate_super_admin!, only: [:destroy] # get /nonprofits/:id # get /:state_code/:city/:name diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index d219c4e2..f171f546 100755 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -3,7 +3,7 @@ class ProfilesController < ApplicationController helper_method :authenticate_profile_owner! - before_filter :authenticate_profile_owner!, only: [:update, :fundraisers, :donations_history] + before_action :authenticate_profile_owner!, only: [:update, :fundraisers, :donations_history] # get /profiles/:id # public profile diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 5dbe31ea..ff211e3b 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -2,7 +2,7 @@ class RolesController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! def create role = Role.create_for_nonprofit(params[:role][:name].to_sym, params[:role][:email], FetchNonprofit.with_params(params)) diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index da64cf2b..a0e60553 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -3,7 +3,7 @@ class SettingsController < ApplicationController include Controllers::NonprofitHelper helper_method :current_nonprofit_user? - before_filter :authenticate_user! + before_action :authenticate_user! def index if current_role?(:super_admin) && params[:nonprofit_id] diff --git a/app/controllers/super_admins_controller.rb b/app/controllers/super_admins_controller.rb index 3d20b636..84ed07a5 100644 --- a/app/controllers/super_admins_controller.rb +++ b/app/controllers/super_admins_controller.rb @@ -2,7 +2,7 @@ class SuperAdminsController < ApplicationController layout "layouts/page" - before_filter :authenticate_super_associate! + before_action :authenticate_super_associate! def index end diff --git a/app/controllers/ticket_levels_controller.rb b/app/controllers/ticket_levels_controller.rb index e0082a0e..3e12ff56 100644 --- a/app/controllers/ticket_levels_controller.rb +++ b/app/controllers/ticket_levels_controller.rb @@ -2,7 +2,7 @@ class TicketLevelsController < ApplicationController include Controllers::EventHelper - before_filter :authenticate_event_editor!, :except => [:index, :show] + before_action :authenticate_event_editor!, :except => [:index, :show] def index ev_id = current_event.id diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 1a8b1569..407961b0 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -3,8 +3,8 @@ class TicketsController < ApplicationController include Controllers::EventHelper helper_method :current_event_admin?, :current_event_editor? - before_filter :authenticate_event_editor!, :except => [:create, :add_note] - before_filter :authenticate_nonprofit_user!, only: [:delete_card_for_ticket] + before_action :authenticate_event_editor!, :except => [:create, :add_note] + before_action :authenticate_nonprofit_user!, only: [:delete_card_for_ticket] # post /nonprofits/:nonprofit_id/events/:event_id/tickets def create From 35c2f5a640f25c07736d7effd092f39550716e5d Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 10:52:26 +0200 Subject: [PATCH 043/440] test(setup): update Devise controller helpers --- gems/grape_devise/spec/spec_helper.rb | 4 ++-- spec/rails_helper.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gems/grape_devise/spec/spec_helper.rb b/gems/grape_devise/spec/spec_helper.rb index dbd0a052..89343208 100644 --- a/gems/grape_devise/spec/spec_helper.rb +++ b/gems/grape_devise/spec/spec_helper.rb @@ -32,5 +32,5 @@ RSpec.configure do |config| config.include Capybara::DSL, type: :request config.include FactoryGirl::Syntax::Methods - config.include Devise::TestHelpers, :type => :controller -end \ No newline at end of file + config.include Devise::Test::ControllerHelpers, type: :controller +end diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 1f9a9a41..701227cb 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -59,6 +59,6 @@ RSpec.configure do |config| config.filter_rails_from_backtrace! # arbitrary gems may also be filtered via: # config.filter_gems_from_backtrace("gem name") - config.include Devise::TestHelpers, :type => :controller + config.include Devise::Test::ControllerHelpers, type: :controller config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: /spec\/api/ end From 11d72b97d508e741123f9780752b72b0eea3c2e8 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 10:52:53 +0200 Subject: [PATCH 044/440] fix(gemfile): add missing rack-ssl dependency --- Gemfile | 1 + Gemfile.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/Gemfile b/Gemfile index 0be6acd4..d996a91c 100755 --- a/Gemfile +++ b/Gemfile @@ -19,6 +19,7 @@ gem 'parallel' gem 'puma' gem 'bootsnap', require: false gem 'rack-timeout' +gem 'rack-ssl' gem 'puma_worker_killer' gem 'test-unit', '~> 3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7620cb74..f1fc81c7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -326,6 +326,8 @@ GEM rack (>= 0.4) rack-attack (5.4.2) rack (>= 1.0, < 3) + rack-ssl (1.4.1) + rack rack-test (0.6.3) rack (>= 1.0) rack-timeout (0.5.1) @@ -532,6 +534,7 @@ DEPENDENCIES qx! rabl rack-attack + rack-ssl rack-timeout rails (= 5.0.7.1) rails-i18n From c3c1311e2f41b7f539a510ef88a1ceb420d2966b Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 10:53:47 +0200 Subject: [PATCH 045/440] test(shared_user_context): move to ActionDistpach From ActionController::TestResponse, to use the new IntegrationTest from rails 5. --- spec/controllers/support/shared_user_context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/controllers/support/shared_user_context.rb b/spec/controllers/support/shared_user_context.rb index 7517f4a1..d36579c1 100644 --- a/spec/controllers/support/shared_user_context.rb +++ b/spec/controllers/support/shared_user_context.rb @@ -93,7 +93,7 @@ RSpec.shared_context :shared_user_context do sign_in user_to_signin if user_to_signin # allows us to run the helpers but ignore what the controller action does # - expect_any_instance_of(described_class).to receive(action).and_return(ActionController::TestResponse.new(200)) + expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(200)) expect_any_instance_of(described_class).to receive(:render).and_return(nil) send(method, action, *args) expect(response.status).to eq 200 @@ -596,4 +596,4 @@ RSpec.shared_context :open_to_profile_owner do |method, action, *args| it 'accepts profile user' do accept(user_with_profile, method, action, *fixed_args) end -end \ No newline at end of file +end From 57125774efd3b18079f461ab7b0a3fe731f4069d Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 11:18:45 +0200 Subject: [PATCH 046/440] test(params): fix HttpPositionalArguments Automatic run with `bundle exec rubocop --rails --only HttpPositionalArguments --auto-correct` --- spec/controllers/campaigns_spec.rb | 4 ++-- spec/requests/maintenance_spec.rb | 8 ++++---- spec/requests/nonprofits/direct_debit_details_spec.rb | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/spec/controllers/campaigns_spec.rb b/spec/controllers/campaigns_spec.rb index 0841b4b6..4673e8b0 100644 --- a/spec/controllers/campaigns_spec.rb +++ b/spec/controllers/campaigns_spec.rb @@ -60,7 +60,7 @@ describe CampaignsController, :type => :controller do describe 'routes' do it "routes campaigns#index" do - expect(:get => "/nonprofits/5/campaigns/4").to(route_to(:controller => "campaigns", :action => "show", nonprofit_id: "5", id: "4")) + expect(get: "/nonprofits/5/campaigns/4").to(route_to(controller: "campaigns", action: "show", nonprofit_id: "5", id: "4")) end end -end \ No newline at end of file +end diff --git a/spec/requests/maintenance_spec.rb b/spec/requests/maintenance_spec.rb index 0a79ef56..fa563ea9 100644 --- a/spec/requests/maintenance_spec.rb +++ b/spec/requests/maintenance_spec.rb @@ -60,7 +60,7 @@ describe 'Maintenance Mode' do end it 'redirects sign_in if the token is wrong' do - get(:new, {maintenance_token: "#{token}3"}) + get(:new, params: { maintenance_token: "#{token}3" }) expect(response.code).to eq "302" expect(response.location).to eq page end @@ -73,18 +73,18 @@ describe 'Maintenance Mode' do it 'redirects sign_in if the token is passed in wrong param' do - get(:new, {maintnancerwrwer_token: "#{token}"}) + get(:new, params: { maintnancerwrwer_token: "#{token}" }) expect(response.code).to eq "302" expect(response.location).to eq page end it 'allows sign_in if the token is passed' do - get(:new, {maintenance_token: "#{token}"}) + get(:new, params: { maintenance_token: "#{token}" }) expect(response.code).to eq '200' end it 'allows sign_in.json' do - get(:new, {maintenance_token: "#{token}", format: 'json'}) + get(:new, params: { maintenance_token: "#{token}", format: 'json' }) expect(response.code).to eq '200' end end diff --git a/spec/requests/nonprofits/direct_debit_details_spec.rb b/spec/requests/nonprofits/direct_debit_details_spec.rb index 4114ad37..35e13b46 100644 --- a/spec/requests/nonprofits/direct_debit_details_spec.rb +++ b/spec/requests/nonprofits/direct_debit_details_spec.rb @@ -18,21 +18,21 @@ describe DirectDebitDetailsController, type: :request do describe 'requires params' do it 'is valid when sepa_params, donation_id and supporter_id are present' do - post "/sepa", valid_params + post "/sepa", params: valid_params assert_response 200 assert_equal nil, JSON.parse(@response.body)["errors"] end it 'is not valid without sepa_params' do - post "/sepa", valid_params.except(:sepa_params) + post "/sepa", params: valid_params.except(:sepa_params) assert_response 422 assert_equal ["sepa_params required"], JSON.parse(@response.body)["errors"] end it 'is not valid without supporter_id' do - post "/sepa", valid_params.except(:supporter_id) + post "/sepa", params: valid_params.except(:supporter_id) assert_response 422 assert_equal ["supporter_id required"], JSON.parse(@response.body)["errors"] From fb638f0c26d9ff5947cbab2ee9e7d239f557dea8 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 13 Jul 2019 11:19:53 +0200 Subject: [PATCH 047/440] test(shared_user_context): fix positional args warning Using positional arguments in functional tests has been deprecated --- spec/controllers/support/shared_user_context.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/controllers/support/shared_user_context.rb b/spec/controllers/support/shared_user_context.rb index d36579c1..c94e0deb 100644 --- a/spec/controllers/support/shared_user_context.rb +++ b/spec/controllers/support/shared_user_context.rb @@ -95,18 +95,22 @@ RSpec.shared_context :shared_user_context do # expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(200)) expect_any_instance_of(described_class).to receive(:render).and_return(nil) - send(method, action, *args) + send(method, action, reduce_params(*args)) expect(response.status).to eq 200 end def reject(user_to_signin, method, action, *args) sign_in user_to_signin if user_to_signin - send(method, action, *args) + send(method, action, reduce_params(*args)) expect(response.status).to eq 302 end alias_method :redirects_to, :reject + def reduce_params(*args) + { params: args.reduce({}, :merge) } + end + def fix_args( *args) replacements = { __our_np: nonprofit.id, From d4f6e2d91d4fc60a5bd6d326bdac095793fb832d Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:06:03 +0200 Subject: [PATCH 048/440] test(donation_context): fix any_instance deprecation warning Changed from the old syntax to the new allow_any_instance_of according to the documentation. https://www.rubydoc.info/github/rspec/rspec-mocks/RSpec/Mocks/ExampleMethods%3aallow_any_instance_of --- spec/support/contexts/shared_rd_donation_value_context.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/support/contexts/shared_rd_donation_value_context.rb b/spec/support/contexts/shared_rd_donation_value_context.rb index 6a95a85c..8a30d2c5 100644 --- a/spec/support/contexts/shared_rd_donation_value_context.rb +++ b/spec/support/contexts/shared_rd_donation_value_context.rb @@ -19,7 +19,7 @@ RSpec.shared_context :shared_rd_donation_value_context do let(:default_edit_token) {'7903e34c-10fe-11e8-9ead-d302c690bee4'} before(:each){ - Event.any_instance.stub(:geocode).and_return([1,1]) + allow_any_instance_of(Event).to_receive(:geocode).and_return([1,1]) } @@ -495,4 +495,4 @@ RSpec.shared_context :shared_rd_donation_value_context do def nil_or_true(item) item.nil? || item end -end \ No newline at end of file +end From e08d7836c663bc9ca995ccb71b6ed697cd1fce35 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:07:15 +0200 Subject: [PATCH 049/440] test(nonprofit): fix xhr deprecation warnings `xhr` and `xml_http_request` are deprecated, changed to new syntax --- spec/api/houdini/nonprofit_spec.rb | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/spec/api/houdini/nonprofit_spec.rb b/spec/api/houdini/nonprofit_spec.rb index 75ab3a7d..396cf775 100644 --- a/spec/api/houdini/nonprofit_spec.rb +++ b/spec/api/houdini/nonprofit_spec.rb @@ -52,15 +52,16 @@ describe Houdini::V1::Nonprofit, :type => :request do e.run Rails.configuration.action_controller.allow_forgery_protection = false } - it 'rejects csrf' do - xhr :post, '/api/v1/nonprofit' + it 'rejects csrf' do + post '/api/v1/nonprofit', params: {}, xhr: true expect(response.code).to eq "401" end end + it 'validates nothing' do input = {} - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "400" expect_validation_errors(JSON.parse(response.body), create_errors("nonprofit", "user")) end @@ -72,7 +73,7 @@ describe Houdini::V1::Nonprofit, :type => :request do phone: "notphone", url: "" }} - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "400" expected = create_errors("user") expected[:errors].push(h(params:["nonprofit[email]"], messages: gr_e("regexp"))) @@ -92,7 +93,7 @@ describe Houdini::V1::Nonprofit, :type => :request do password_confirmation: 'doesn\'t match' } } - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "400" expect(JSON.parse(response.body)['errors']).to include(h(params:["user[password]", "user[password_confirmation]"], messages: gr_e("is_equal_to"))) @@ -107,7 +108,7 @@ describe Houdini::V1::Nonprofit, :type => :request do expect_any_instance_of(SlugNonprofitNamingAlgorithm).to receive(:create_copy_name).and_raise(UnableToCreateNameCopyError.new) - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "400" expect_validation_errors(JSON.parse(response.body), { @@ -128,7 +129,7 @@ describe Houdini::V1::Nonprofit, :type => :request do user: {name: "Name", email: "em@em.com", password: "12345678", password_confirmation: "12345678"} } - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "400" expect_validation_errors(JSON.parse(response.body), { @@ -155,7 +156,7 @@ describe Houdini::V1::Nonprofit, :type => :request do #expect(Houdini::V1::Nonprofit).to receive(:sign_in) - xhr :post, '/api/v1/nonprofit', input + post '/api/v1/nonprofit', params: input, xhr: true expect(response.code).to eq "201" our_np = Nonprofit.all[1] @@ -211,4 +212,4 @@ end def gr_e(*keys) keys.map {|i| I18n.translate("grape.errors.messages." + i, locale: 'en')} -end \ No newline at end of file +end From 812d54608030aa539c74f59531b984d3c2bd996d Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:48:23 +0200 Subject: [PATCH 050/440] fix(app): fixnum is deprecated Use Integer, since Fixnum and Bigint now belong to the same class in Ruby 2.4+ --- gems/ruby-qx/lib/qx.rb | 2 +- lib/qexpr.rb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/gems/ruby-qx/lib/qx.rb b/gems/ruby-qx/lib/qx.rb index 1db46b59..5434c1e8 100644 --- a/gems/ruby-qx/lib/qx.rb +++ b/gems/ruby-qx/lib/qx.rb @@ -455,7 +455,7 @@ class Qx # Quote a string for use in sql to prevent injection or weird errors # Always use this for all values! # Just uses double-dollar quoting universally. Should be generally safe and easy. - # Will return an unquoted value it it's a Fixnum + # Will return an unquoted value it it's a Integer def self.quote(val) if val.is_a?(Qx) val.parse diff --git a/lib/qexpr.rb b/lib/qexpr.rb index f66b14ee..e0948527 100644 --- a/lib/qexpr.rb +++ b/lib/qexpr.rb @@ -207,9 +207,9 @@ class Qexpr # Quote a string for use in sql to prevent injection or weird errors # Always use this for all values! # Just uses double-dollar quoting universally. Should be generally safe and easy. - # Will return an unquoted value it it's a Fixnum + # Will return an unquoted value it it's a Integer def self.quote(val) - if val.is_a?(Fixnum) || (val.is_a?(String) && val =~ /^\$Q\$.+\$Q\$$/) # is a valid num or already quoted + if val.is_a?(Integer) || (val.is_a?(String) && val =~ /^\$Q\$.+\$Q\$$/) # is a valid num or already quoted val elsif val == nil "NULL" From 4992253540b0f729f05f6b23c1295e437c37141a Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:49:01 +0200 Subject: [PATCH 051/440] fix(constants): explicit require for constants --- app/models/recurring_donation.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index 55e585fd..c6002b96 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,4 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require_dependency 'lib/timespan' + class RecurringDonation < ApplicationRecord #TODO: From 22bf0713f90b73c8aa73b4922ace0f1598991cef Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:49:40 +0200 Subject: [PATCH 052/440] fix(campaign): add explicit source for payments relation --- app/models/campaign.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 5aaa9162..137381c1 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -57,7 +57,7 @@ class Campaign < ApplicationRecord has_many :donations has_many :charges, through: :donations - has_many :payments, through: :donations + has_many :payments, { through: :donations, source: "payment" } has_many :campaign_gift_options has_many :campaign_gifts, through: :campaign_gift_options has_many :supporters, :through => :donations From a20bd477ce2d8addd9bee8aaa17f5d1a9b00ca5c Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 10:50:27 +0200 Subject: [PATCH 053/440] fix(static_controller): return body nil when 500 Fix deprecation warning from nothing. --- app/controllers/static_controller.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index e0587fb0..6598e0f2 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -14,7 +14,7 @@ class StaticController < ApplicationController if result send_file(temp_file, :type => "application/gzip") else - render :nothing => true, :status => 500 + render body: nil, status: 500 end elsif (ccs_method == 'github') git_hash = File.read("#{Rails.root}/CCS_HASH") From 33862bab25650de2074720b01dbb86423a0b64d9 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 11:25:00 +0200 Subject: [PATCH 054/440] test(support): fix any_instance deprecation warning Changed from the old syntax to the new allow_any_instance_of according to the documentation. https://www.rubydoc.info/github/rspec/rspec-mocks/RSpec/Mocks/ExampleMethods%3aallow_any_instance_of --- spec/lib/update/update_tickets_spec.rb | 2 +- spec/support/contexts/shared_donation_charge_context.rb | 6 +++--- spec/support/contexts/shared_rd_donation_value_context.rb | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/spec/lib/update/update_tickets_spec.rb b/spec/lib/update/update_tickets_spec.rb index 7c8df4e1..da844a6e 100644 --- a/spec/lib/update/update_tickets_spec.rb +++ b/spec/lib/update/update_tickets_spec.rb @@ -5,7 +5,7 @@ describe UpdateTickets do let(:ticket) { - Event.any_instance.stub(:geocode).and_return([1,1]) + allow_any_instance_of(Event).to receive(:geocode).and_return([1,1]) create(:ticket, :has_card, :has_event) } diff --git a/spec/support/contexts/shared_donation_charge_context.rb b/spec/support/contexts/shared_donation_charge_context.rb index 2e0d2a63..1e2d1037 100644 --- a/spec/support/contexts/shared_donation_charge_context.rb +++ b/spec/support/contexts/shared_donation_charge_context.rb @@ -17,11 +17,11 @@ RSpec.shared_context :shared_donation_charge_context do let(:campaign) {force_create(:campaign, nonprofit: nonprofit, goal_amount: 500)} let(:other_campaign) {force_create(:campaign, nonprofit: other_nonprofit)} let(:event) { - Event.any_instance.stub(:geocode).and_return([1,1]) + allow_any_instance_of(Event).to receive(:geocode).and_return([1,1]) force_create(:event, nonprofit: nonprofit) } let(:other_event) { - Event.any_instance.stub(:geocode).and_return([1,1]) + allow_any_instance_of(Event).to receive(:geocode).and_return([1,1]) force_create(:event, nonprofit: other_nonprofit) } let(:event_discount) {force_create(:event_discount, event: event, percent: 20)} @@ -46,4 +46,4 @@ RSpec.shared_context :shared_donation_charge_context do StripeMock.stop end } -end \ No newline at end of file +end diff --git a/spec/support/contexts/shared_rd_donation_value_context.rb b/spec/support/contexts/shared_rd_donation_value_context.rb index 8a30d2c5..a8889e82 100644 --- a/spec/support/contexts/shared_rd_donation_value_context.rb +++ b/spec/support/contexts/shared_rd_donation_value_context.rb @@ -19,7 +19,7 @@ RSpec.shared_context :shared_rd_donation_value_context do let(:default_edit_token) {'7903e34c-10fe-11e8-9ead-d302c690bee4'} before(:each){ - allow_any_instance_of(Event).to_receive(:geocode).and_return([1,1]) + allow_any_instance_of(Event).to receive(:geocode).and_return([1,1]) } From d153de8d1f5b99d1bb2c5195bf19a2b86f69a6f3 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sun, 14 Jul 2019 11:29:19 +0200 Subject: [PATCH 055/440] fix(dependency): Remove require dependency for timespan constants --- app/models/recurring_donation.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index c6002b96..8833b076 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,5 +1,4 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require_dependency 'lib/timespan' class RecurringDonation < ApplicationRecord From eb7c11d849bae3b787ad2c3245852ff61678ed08 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 20 Jul 2019 15:07:19 +0200 Subject: [PATCH 056/440] chore(gems): update rails and ruby version to latest --- Gemfile | 4 +- Gemfile.lock | 112 ++++++++++-------- config/initializers/new_framework_defaults.rb | 25 ---- 3 files changed, 62 insertions(+), 79 deletions(-) delete mode 100644 config/initializers/new_framework_defaults.rb diff --git a/Gemfile b/Gemfile index d996a91c..76758641 100755 --- a/Gemfile +++ b/Gemfile @@ -1,8 +1,8 @@ source 'https://rubygems.org' -ruby '2.4.5' +ruby '2.5.1' gem 'rake' -gem 'rails', '= 5.0.7.1' +gem 'rails', '= 5.2.3' # https://stripe.com/docs/api gem 'stripe' diff --git a/Gemfile.lock b/Gemfile.lock index f1fc81c7..702689c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -33,39 +33,43 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.0.0) - actioncable (5.0.7.1) - actionpack (= 5.0.7.1) - nio4r (>= 1.2, < 3.0) - websocket-driver (~> 0.6.1) - actionmailer (5.0.7.1) - actionpack (= 5.0.7.1) - actionview (= 5.0.7.1) - activejob (= 5.0.7.1) + actioncable (5.2.3) + actionpack (= 5.2.3) + nio4r (~> 2.0) + websocket-driver (>= 0.6.1) + actionmailer (5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.0.7.1) - actionview (= 5.0.7.1) - activesupport (= 5.0.7.1) + actionpack (5.2.3) + actionview (= 5.2.3) + activesupport (= 5.2.3) rack (~> 2.0) - rack-test (~> 0.6.3) + rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.0.7.1) - activesupport (= 5.0.7.1) + actionview (5.2.3) + activesupport (= 5.2.3) builder (~> 3.1) - erubis (~> 2.7.0) + erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.0.7.1) - activesupport (= 5.0.7.1) + activejob (5.2.3) + activesupport (= 5.2.3) globalid (>= 0.3.6) - activemodel (5.0.7.1) - activesupport (= 5.0.7.1) - activerecord (5.0.7.1) - activemodel (= 5.0.7.1) - activesupport (= 5.0.7.1) - arel (~> 7.0) - activesupport (5.0.7.1) + activemodel (5.2.3) + activesupport (= 5.2.3) + activerecord (5.2.3) + activemodel (= 5.2.3) + activesupport (= 5.2.3) + arel (>= 9.0) + activestorage (5.2.3) + actionpack (= 5.2.3) + activerecord (= 5.2.3) + marcel (~> 0.3.1) + activesupport (5.2.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -74,7 +78,7 @@ GEM public_suffix (>= 2.0.2, < 4.0) amq-protocol (2.3.0) andand (1.3.3) - arel (7.1.4) + arel (9.0.0) ast (2.4.0) aws-eventstream (1.0.1) aws-partitions (1.110.0) @@ -126,7 +130,7 @@ GEM coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.8.1) - concurrent-ruby (1.1.4) + concurrent-ruby (1.1.5) config (1.7.0) activesupport (>= 3.0) deep_merge (~> 1.2.1) @@ -201,7 +205,7 @@ GEM dry-logic (~> 0.4, >= 0.4.0) dry-types (~> 0.13.1) equalizer (0.0.11) - erubis (2.7.0) + erubi (1.8.0) execjs (2.7.0) factory_bot (4.11.1) activesupport (>= 3.0.0) @@ -255,7 +259,7 @@ GEM domain_name (~> 0.5) httparty (0.16.2) multi_xml (>= 0.5.2) - i18n (1.5.3) + i18n (1.6.0) concurrent-ruby (~> 1.0) i18n-js (3.1.0) i18n (>= 0.6.6, < 2) @@ -278,13 +282,16 @@ GEM mini_mime (>= 0.1.1) mail_view (2.0.4) tilt + marcel (0.3.3) + mimemagic (~> 0.3.2) memcachier (0.0.2) method_source (0.9.2) mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2018.0812) + mimemagic (0.3.3) mini_magick (4.9.2) - mini_mime (1.0.1) + mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.11.3) money (6.13.1) @@ -301,8 +308,8 @@ GEM kdtree require_all netrc (0.11.0) - nio4r (2.3.1) - nokogiri (1.10.1) + nio4r (2.4.0) + nokogiri (1.10.3) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) parallel (1.13.0) @@ -321,27 +328,28 @@ GEM puma (>= 2.7, < 4) rabl (0.14.0) activesupport (>= 2.3.14) - rack (2.0.6) + rack (2.0.7) rack-accept (0.4.5) rack (>= 0.4) rack-attack (5.4.2) rack (>= 1.0, < 3) rack-ssl (1.4.1) rack - rack-test (0.6.3) - rack (>= 1.0) + rack-test (1.1.0) + rack (>= 1.0, < 3) rack-timeout (0.5.1) - rails (5.0.7.1) - actioncable (= 5.0.7.1) - actionmailer (= 5.0.7.1) - actionpack (= 5.0.7.1) - actionview (= 5.0.7.1) - activejob (= 5.0.7.1) - activemodel (= 5.0.7.1) - activerecord (= 5.0.7.1) - activesupport (= 5.0.7.1) + rails (5.2.3) + actioncable (= 5.2.3) + actionmailer (= 5.2.3) + actionpack (= 5.2.3) + actionview (= 5.2.3) + activejob (= 5.2.3) + activemodel (= 5.2.3) + activerecord (= 5.2.3) + activestorage (= 5.2.3) + activesupport (= 5.2.3) bundler (>= 1.3.0) - railties (= 5.0.7.1) + railties (= 5.2.3) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) @@ -351,12 +359,12 @@ GEM rails-i18n (5.1.3) i18n (>= 0.7, < 2) railties (>= 5.0, < 6) - railties (5.0.7.1) - actionpack (= 5.0.7.1) - activesupport (= 5.0.7.1) + railties (5.2.3) + actionpack (= 5.2.3) + activesupport (= 5.2.3) method_source rake (>= 0.8.7) - thor (>= 0.18.1, < 2.0) + thor (>= 0.19.0, < 2.0) rainbow (3.0.0) rake (12.3.2) request_store (1.4.1) @@ -473,9 +481,9 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff - websocket-driver (0.6.5) + websocket-driver (0.7.1) websocket-extensions (>= 0.1.0) - websocket-extensions (0.1.3) + websocket-extensions (0.1.4) xml-simple (1.1.5) yard (0.9.18) @@ -536,7 +544,7 @@ DEPENDENCIES rack-attack rack-ssl rack-timeout - rails (= 5.0.7.1) + rails (= 5.2.3) rails-i18n rake roadie-rails @@ -559,7 +567,7 @@ DEPENDENCIES webmock RUBY VERSION - ruby 2.4.5p335 + ruby 2.5.1p57 BUNDLED WITH 1.17.3 diff --git a/config/initializers/new_framework_defaults.rb b/config/initializers/new_framework_defaults.rb deleted file mode 100644 index cbf423a8..00000000 --- a/config/initializers/new_framework_defaults.rb +++ /dev/null @@ -1,25 +0,0 @@ -# Be sure to restart your server when you modify this file. -# -# This file contains migration options to ease your Rails 5.0 upgrade. -# -# Once upgraded flip defaults one by one to migrate to the new default. -# -# Read the Guide for Upgrading Ruby on Rails for more info on each option. - -Rails.application.config.action_controller.raise_on_unfiltered_parameters = true - -# Enable per-form CSRF tokens. Previous versions had false. -Rails.application.config.action_controller.per_form_csrf_tokens = false - -# Enable origin-checking CSRF mitigation. Previous versions had false. -Rails.application.config.action_controller.forgery_protection_origin_check = false - -# Make Ruby 2.4 preserve the timezone of the receiver when calling `to_time`. -# Previous versions had false. -ActiveSupport.to_time_preserves_timezone = false - -# Require `belongs_to` associations by default. Previous versions had false. -Rails.application.config.active_record.belongs_to_required_by_default = false - -# Do not halt callback chains when a callback returns false. Previous versions had true. -ActiveSupport.halt_callback_chains_on_return_false = true From 9f6ea912240706097825e665d3d3adb66b4a7fc0 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 20 Jul 2019 15:10:32 +0200 Subject: [PATCH 057/440] fix(direct_debit_detail): use class_name string value --- app/models/direct_debit_detail.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/direct_debit_detail.rb b/app/models/direct_debit_detail.rb index d4dbfdcb..85b83947 100644 --- a/app/models/direct_debit_detail.rb +++ b/app/models/direct_debit_detail.rb @@ -4,5 +4,5 @@ class DirectDebitDetail < ApplicationRecord has_many :donations has_many :charges - belongs_to :holder, class_name: Supporter + belongs_to :holder, class_name: Supporter.class_name end From 84f8a583ba8b21397f4da56833f36cf63fdebe8a Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 23 Jul 2019 21:20:54 +0200 Subject: [PATCH 058/440] test(shared_user_ctx): add conditional specs for types of methods --- app/controllers/nonprofits_controller.rb | 2 +- .../support/shared_user_context.rb | 22 +++++++++++++------ 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index 93893eb3..55aa04ab 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -30,7 +30,7 @@ respond_to do |format| format.html - format.json {render json: @nonprofit} + format.json {@nonprofit} end end diff --git a/spec/controllers/support/shared_user_context.rb b/spec/controllers/support/shared_user_context.rb index c94e0deb..3abe31f6 100644 --- a/spec/controllers/support/shared_user_context.rb +++ b/spec/controllers/support/shared_user_context.rb @@ -90,13 +90,21 @@ RSpec.shared_context :shared_user_context do end def accept(user_to_signin, method, action, *args) + without_json_response = [:cancellation, :all_npos].include?(action) + request.accept = "application/json" unless without_json_response sign_in user_to_signin if user_to_signin # allows us to run the helpers but ignore what the controller action does - # - expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(200)) - expect_any_instance_of(described_class).to receive(:render).and_return(nil) - send(method, action, reduce_params(*args)) - expect(response.status).to eq 200 + + if without_json_response + expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(200)) + expect_any_instance_of(described_class).to receive(:render).and_return(nil) + send(method, action, reduce_params(*args)) + expect(response.status).to eq 200 + else + expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(204)) + send(method, action, reduce_params(*args)) + expect(response.status).to eq 204 + end end def reject(user_to_signin, method, action, *args) @@ -111,7 +119,7 @@ RSpec.shared_context :shared_user_context do { params: args.reduce({}, :merge) } end - def fix_args( *args) + def fix_args(*args) replacements = { __our_np: nonprofit.id, __our_campaign: campaign.id, @@ -198,7 +206,7 @@ end RSpec.shared_context :open_to_np_associate do |method, action, *args| include_context :shared_user_context let(:fixed_args){ - fix_args( *args) + fix_args(*args) } it 'rejects no user' do From df29d446aeb8e4804d91992cb0efb86e1fc635cc Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 23 Jul 2019 21:22:00 +0200 Subject: [PATCH 059/440] chore(models): comment out attr_accessible Needs to be changed to strong params in controllers of each model. --- app/models/coupon.rb | 12 ++++++------ app/models/custom_field_master.rb | 10 +++++----- app/models/direct_debit_detail.rb | 2 +- app/models/email_list.rb | 2 +- app/models/export.rb | 2 +- app/models/offsite_payment.rb | 2 +- app/models/payment_import.rb | 2 +- app/models/source_token.rb | 2 +- app/models/ticket.rb | 2 +- app/models/tracking.rb | 2 +- 10 files changed, 19 insertions(+), 19 deletions(-) diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 08d93729..5b8b5f7c 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -1,12 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Coupon < ApplicationRecord - attr_accessible \ - :name, - :victim_np_id, - :paid, # boolean - :nonprofit, :nonprofit_id + # attr_accessible \ + # :name, + # :victim_np_id, + # :paid, # boolean + # :nonprofit, :nonprofit_id scope :unpaid, -> {where(paid: [nil,false])} validates_presence_of :name, :nonprofit_id, :victim_np_id -end \ No newline at end of file +end diff --git a/app/models/custom_field_master.rb b/app/models/custom_field_master.rb index 56f4398a..b528b373 100644 --- a/app/models/custom_field_master.rb +++ b/app/models/custom_field_master.rb @@ -1,11 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CustomFieldMaster < ApplicationRecord - attr_accessible \ - :nonprofit, :nonprofit_id, - :name, - :deleted, - :created_at + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :created_at validates :name, presence: true validate :no_dupes, on: :create diff --git a/app/models/direct_debit_detail.rb b/app/models/direct_debit_detail.rb index 85b83947..a8e96676 100644 --- a/app/models/direct_debit_detail.rb +++ b/app/models/direct_debit_detail.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class DirectDebitDetail < ApplicationRecord - attr_accessible :iban, :account_holder_name, :bic, :supporter_id, :holder + # attr_accessible :iban, :account_holder_name, :bic, :supporter_id, :holder has_many :donations has_many :charges diff --git a/app/models/email_list.rb b/app/models/email_list.rb index 4f8fccf1..f60af7d0 100644 --- a/app/models/email_list.rb +++ b/app/models/email_list.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailList < ApplicationRecord - attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master + # attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master belongs_to :nonprofit belongs_to :tag_master end diff --git a/app/models/export.rb b/app/models/export.rb index ee4717a4..83f066c2 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -2,7 +2,7 @@ class Export < ApplicationRecord STATUS = %w[queued started completed failed].freeze - attr_accessible :exception, :nonprofit, :status, :user, :export_type, :parameters, :ended, :url, :user_id, :nonprofit_id + # attr_accessible :exception, :nonprofit, :status, :user, :export_type, :parameters, :ended, :url, :user_id, :nonprofit_id belongs_to :nonprofit belongs_to :user diff --git a/app/models/offsite_payment.rb b/app/models/offsite_payment.rb index 9a729ed2..798ef1c9 100644 --- a/app/models/offsite_payment.rb +++ b/app/models/offsite_payment.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class OffsitePayment < ApplicationRecord - attr_accessible :gross_amount, :kind, :date, :check_number + # attr_accessible :gross_amount, :kind, :date, :check_number belongs_to :payment, dependent: :destroy belongs_to :donation belongs_to :nonprofit diff --git a/app/models/payment_import.rb b/app/models/payment_import.rb index fea614a5..5980928f 100644 --- a/app/models/payment_import.rb +++ b/app/models/payment_import.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class PaymentImport < ApplicationRecord - attr_accessible :nonprofit, :user + # attr_accessible :nonprofit, :user has_and_belongs_to_many :donations belongs_to :nonprofit belongs_to :user diff --git a/app/models/source_token.rb b/app/models/source_token.rb index 1d6ca55f..159d682f 100644 --- a/app/models/source_token.rb +++ b/app/models/source_token.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SourceToken < ApplicationRecord self.primary_key = :token - attr_accessible :expiration, :token, :max_uses, :total_uses + # attr_accessible :expiration, :token, :max_uses, :total_uses belongs_to :tokenizable, :polymorphic => true belongs_to :event end diff --git a/app/models/ticket.rb b/app/models/ticket.rb index 9e56fd2b..ee5ad133 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1,7 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Ticket < ApplicationRecord - attr_accessible :note, :event_discount, :event_discount_id + # attr_accessible :note, :event_discount, :event_discount_id belongs_to :event_discount belongs_to :supporter diff --git a/app/models/tracking.rb b/app/models/tracking.rb index a730989f..0903b3e3 100644 --- a/app/models/tracking.rb +++ b/app/models/tracking.rb @@ -1,6 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Tracking < ApplicationRecord - attr_accessible :utm_campaign, :utm_content, :utm_medium, :utm_source + # attr_accessible :utm_campaign, :utm_content, :utm_medium, :utm_source belongs_to :donation end From 28a0793377e9a70dcbff2e44f24147f6da6839bd Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 23 Jul 2019 21:23:14 +0200 Subject: [PATCH 060/440] feat(campaigns): return expected api response --- lib/create/create_peer_to_peer_campaign.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/create/create_peer_to_peer_campaign.rb b/lib/create/create_peer_to_peer_campaign.rb index 854ca4db..032de0ab 100644 --- a/lib/create/create_peer_to_peer_campaign.rb +++ b/lib/create/create_peer_to_peer_campaign.rb @@ -28,6 +28,6 @@ module CreatePeerToPeerCampaign return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? - campaign.as_json + campaign.as_json['campaign'] end end From cf5792d4e5888fc6cdb8f8f6290907dad85f5353 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 27 Jul 2019 14:39:40 +0200 Subject: [PATCH 061/440] feat(gems): update Gemfile --- Gemfile | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/Gemfile b/Gemfile index 76758641..0a5dda0f 100755 --- a/Gemfile +++ b/Gemfile @@ -37,8 +37,10 @@ gem 'sprockets' gem 'font_assets', '~> 0.1.14' # Database (postgres) -gem 'pg' # Postgresql + +gem 'pg', '~> 0.11' gem 'qx', path: 'gems/ruby-qx' + gem 'dalli' gem 'memcachier' @@ -72,9 +74,6 @@ gem 'httparty' gem 'devise', '~> 4.4' gem 'devise-async' -# https://github.com/airbrake/airbrake -gem 'airbrake', '~> 8.0.1' - # http://www.rubygeocoder.com/ gem 'geocoder' # for adding latitude and longitude to location-based tables @@ -142,9 +141,4 @@ gem 'grape-swagger' gem 'grape-swagger-entity' gem 'grape_url_validator' gem 'grape_logging' -gem 'grape_devise' -#gem 'grape_devise', git: 'https://github.com/ericschultz/grape_devise.git' - -#gem 'protected_attributes' - -gem 'rack-ssl' +gem 'grape_devise', path: 'gems/grape_devise' From 1c28460ad7d2da2d2c9a200733960148ebf6f7f7 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Sat, 27 Jul 2019 14:40:08 +0200 Subject: [PATCH 062/440] feat(api): use active support reloader instead of callback --- config/initializers/reload_api.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/reload_api.rb b/config/initializers/reload_api.rb index 2de95e95..41e476bc 100644 --- a/config/initializers/reload_api.rb +++ b/config/initializers/reload_api.rb @@ -5,7 +5,7 @@ if Rails.env.development? api_reloader = ActiveSupport::FileUpdateChecker.new(api_files) do Rails.application.reload_routes! end - ActionDispatch::Callbacks.to_prepare do + ActiveSupport::Reloader.to_prepare do api_reloader.execute_if_updated end -end \ No newline at end of file +end From 7c1f78e7d67f550617dd2a2881c1936026b99197 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 29 Jul 2019 20:11:14 +0200 Subject: [PATCH 063/440] docs(getting_started): add newest version of ruby --- .ruby-version | 2 +- docs/GETTING_STARTED.MD | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 59aa62c1..73462a5a 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.4.5 +2.5.1 diff --git a/docs/GETTING_STARTED.MD b/docs/GETTING_STARTED.MD index a5c822e1..370eb8e4 100644 --- a/docs/GETTING_STARTED.MD +++ b/docs/GETTING_STARTED.MD @@ -6,7 +6,7 @@ You'll need to have in your Mac the following dependencies installed, if you don't want to use the provided Docker containers. -* Ruby `2.4.5` +* Ruby `2.5.1` * Rails `5.0.7.1` * Node `11.12.0` From da68bca689000980649c00e400497a7bb9bc2e5f Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 29 Jul 2019 20:40:30 +0200 Subject: [PATCH 064/440] docs(getting_started): add known bug for Qx gem at rake db setup --- docs/GETTING_STARTED.MD | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/GETTING_STARTED.MD b/docs/GETTING_STARTED.MD index 370eb8e4..33a26757 100644 --- a/docs/GETTING_STARTED.MD +++ b/docs/GETTING_STARTED.MD @@ -44,6 +44,16 @@ The last secrets you'll need are related to AWS. You can learn how to [create an Run `rake db:setup` to run all the db tasks within one command. This will create the dbs for each environment, load the `structure.sql`, run pending migrations and will also run the seed functionality. +------- + +**Known problems** +If you encounter `database doesnt exist in rake db create` after running both `rake db:setup` and `rake db:create`, you'll need to comment out the lines these lines at `pg_type_map.rb` +``` +Qx.config(type_map: PG::BasicTypeMapForResults.new(ActiveRecord::Base.connection.raw_connection)) +Qx.execute("SET TIME ZONE utc") +``` + + ### How to run You'll need 2 consoles to run the project. One for the rails env and another one to run the asset pipeline through [webpack](https://webpack.js.org) , since it's *not incorporated yet* into the rails asset pipeline. From 04a5eb039fd2b41e65912c11466e9ed146523103 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Mon, 29 Jul 2019 20:44:35 +0200 Subject: [PATCH 065/440] feat(pg_type_map): add validation for Qx to run if database exists --- config/initializers/pg_type_map.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/config/initializers/pg_type_map.rb b/config/initializers/pg_type_map.rb index 9f4c07f8..1072e823 100644 --- a/config/initializers/pg_type_map.rb +++ b/config/initializers/pg_type_map.rb @@ -2,6 +2,12 @@ require 'active_record' require 'qx' require 'pg' -Qx.config(type_map: PG::BasicTypeMapForResults.new(ActiveRecord::Base.connection.raw_connection)) -Qx.execute("SET TIME ZONE utc") +def database_exists? + ActiveRecord::Base.connection +rescue ActiveRecord::NoDatabaseError + false +else + Qx.config(type_map: PG::BasicTypeMapForResults.new(ActiveRecord::Base.connection.raw_connection)) + Qx.execute("SET TIME ZONE utc") +end From f0fd393be485d18fa8bf2c4b7ddb9afe18a2bee5 Mon Sep 17 00:00:00 2001 From: Luis Castro Date: Tue, 30 Jul 2019 23:29:24 +0200 Subject: [PATCH 066/440] style(format): run rubocop format autocorrect --- Gemfile | 50 +- Rakefile | 4 +- app/api/houdini/api.rb | 4 +- app/api/houdini/v1/api.rb | 36 +- app/api/houdini/v1/base_api.rb | 6 +- app/api/houdini/v1/entities/nonprofit.rb | 4 +- .../houdini/v1/entities/validation_error.rb | 8 +- .../houdini/v1/entities/validation_errors.rb | 6 +- .../houdini/v1/helpers/application_helper.rb | 18 +- app/api/houdini/v1/helpers/rescue_helper.rb | 21 +- app/api/houdini/v1/nonprofit.rb | 139 ++-- app/api/houdini/v1/validations.rb | 4 +- app/api/houdini/v1/validators/is_equal_to.rb | 6 +- app/controllers/activities_controller.rb | 12 +- app/controllers/application_controller.rb | 232 +++--- .../aws_presigned_posts_controller.rb | 7 +- .../billing_subscriptions_controller.rb | 28 +- app/controllers/button_debug_controller.rb | 6 +- .../campaign_gift_options_controller.rb | 48 +- app/controllers/campaign_gifts_controller.rb | 9 +- .../campaign_gift_options_controller.rb | 80 +- .../campaigns/donations_controller.rb | 29 +- .../campaigns/supporters_controller.rb | 33 +- app/controllers/campaigns_controller.rb | 25 +- app/controllers/cards_controller.rb | 16 +- .../direct_debit_details_controller.rb | 8 +- app/controllers/email_settings_controller.rb | 4 +- app/controllers/emails_controller.rb | 15 +- app/controllers/event_discounts_controller.rb | 19 +- app/controllers/events_controller.rb | 59 +- app/controllers/front_controller.rb | 22 +- .../image_attachments_controller.rb | 42 +- app/controllers/maps_controller.rb | 52 +- .../nonprofits/activities_controller.rb | 12 +- .../nonprofits/bank_accounts_controller.rb | 109 +-- .../nonprofits/button_controller.rb | 39 +- .../nonprofits/cards_controller.rb | 9 +- .../nonprofits/charges_controller.rb | 19 +- .../custom_field_joins_controller.rb | 64 +- .../custom_field_masters_controller.rb | 42 +- .../nonprofits/donations_controller.rb | 125 ++- .../nonprofits/email_lists_controller.rb | 22 +- .../nonprofits/imports_controller.rb | 10 +- .../miscellaneous_np_infos_controller.rb | 13 +- .../nonprofits/nonprofit_keys_controller.rb | 63 +- .../nonprofits/payments_controller.rb | 44 +- .../nonprofits/payouts_controller.rb | 81 +- .../recurring_donations_controller.rb | 167 ++-- .../nonprofits/refunds_controller.rb | 31 +- .../nonprofits/reports_controller.rb | 14 +- .../nonprofits/supporter_emails_controller.rb | 11 +- .../nonprofits/supporter_notes_controller.rb | 41 +- .../nonprofits/supporters_controller.rb | 192 ++--- .../nonprofits/tag_joins_controller.rb | 40 +- .../nonprofits/tag_masters_controller.rb | 14 +- .../nonprofits/trackings_controller.rb | 8 +- app/controllers/nonprofits_controller.rb | 131 ++-- app/controllers/onboard_controller.rb | 2 + app/controllers/profiles_controller.rb | 75 +- .../recurring_donations_controller.rb | 20 +- app/controllers/roles_controller.rb | 36 +- app/controllers/settings_controller.rb | 44 +- app/controllers/static_controller.rb | 9 +- app/controllers/super_admins_controller.rb | 51 +- app/controllers/ticket_levels_controller.rb | 49 +- app/controllers/tickets_controller.rb | 42 +- .../users/confirmations_controller.rb | 52 +- .../users/registrations_controller.rb | 14 +- app/controllers/users/sessions_controller.rb | 49 +- app/helpers/application_helper.rb | 107 +-- app/helpers/card_helper.rb | 37 +- app/helpers/devise_helper.rb | 4 +- app/helpers/nonprofits_helper.rb | 26 +- app/helpers/onboard_helper.rb | 2 + app/helpers/pricing_helper.rb | 14 +- app/helpers/profiles_helper.rb | 6 +- app/jobs/application_job.rb | 2 + app/mailers/admin_mailer.rb | 3 +- app/mailers/base_mailer.rb | 4 +- app/mailers/billing_subscription_mailer.rb | 8 +- app/mailers/campaign_mailer.rb | 23 +- app/mailers/donation_mailer.rb | 157 ++-- app/mailers/event_mailer.rb | 18 +- app/mailers/export_mailer.rb | 4 +- app/mailers/generic_mailer.rb | 8 +- app/mailers/import_mailer.rb | 14 +- app/mailers/nonprofit_admin_mailer.rb | 46 +- app/mailers/nonprofit_mailer.rb | 140 ++-- app/mailers/payment_mailer.rb | 4 +- app/mailers/recurring_donation_mailer.rb | 21 +- app/mailers/testing.rb | 4 +- app/mailers/ticket_mailer.rb | 15 +- app/mailers/user_mailer.rb | 36 +- app/models/activity.rb | 4 +- app/models/application_record.rb | 6 +- app/models/bank_account.rb | 67 +- app/models/billing_plan.rb | 28 +- app/models/billing_subscription.rb | 49 +- app/models/campaign.rb | 297 ++++---- app/models/campaign_gift.rb | 24 +- app/models/campaign_gift_option.rb | 58 +- app/models/card.rb | 40 +- app/models/charge.rb | 57 +- app/models/comment.rb | 56 +- app/models/coupon.rb | 16 +- app/models/custom_field_join.rb | 35 +- app/models/custom_field_master.rb | 32 +- app/models/direct_debit_detail.rb | 2 + app/models/dispute.rb | 12 +- app/models/donation.rb | 87 +-- app/models/email_draft.rb | 22 +- app/models/email_list.rb | 2 + app/models/email_setting.rb | 6 +- app/models/event.rb | 177 +++-- app/models/event_discount.rb | 5 +- app/models/export.rb | 3 +- app/models/full_contact_info.rb | 40 +- app/models/full_contact_org.rb | 8 +- app/models/full_contact_photo.rb | 22 +- app/models/full_contact_social_profile.rb | 26 +- app/models/full_contact_topic.rb | 8 +- app/models/image_attachment.rb | 12 +- app/models/import.rb | 29 +- app/models/miscellaneous_np_info.rb | 5 +- app/models/nonprofit.rb | 78 +- app/models/nonprofit_account.rb | 18 +- app/models/offsite_payment.rb | 14 +- app/models/payment.rb | 44 +- app/models/payment_import.rb | 2 + app/models/payment_payout.rb | 26 +- app/models/payout.rb | 86 +-- app/models/profile.rb | 189 ++--- app/models/recurring_donation.rb | 39 +- app/models/refund.rb | 39 +- app/models/role.rb | 90 ++- app/models/source_token.rb | 4 +- app/models/supporter.rb | 30 +- app/models/supporter_email.rb | 28 +- app/models/supporter_note.rb | 20 +- app/models/tag_join.rb | 35 +- app/models/tag_master.rb | 36 +- app/models/ticket.rb | 29 +- app/models/ticket_level.rb | 16 +- app/models/tracking.rb | 2 + app/models/user.rb | 154 ++-- app/uploaders/article_background_uploader.rb | 11 +- app/uploaders/article_uploader.rb | 10 +- .../campaign_background_image_uploader.rb | 33 +- .../campaign_banner_image_uploader.rb | 22 +- app/uploaders/campaign_main_image_uploader.rb | 88 +-- .../event_background_image_uploader.rb | 10 +- app/uploaders/event_main_image_uploader.rb | 88 +-- app/uploaders/image_attachment_uploader.rb | 16 +- .../nonprofit_background_uploader.rb | 10 +- app/uploaders/nonprofit_logo_uploader.rb | 78 +- app/uploaders/nonprofit_uploader.rb | 14 +- app/uploaders/profile_uploader.rb | 94 +-- app/views/campaigns/index.rabl | 7 +- app/views/events/index.rabl | 6 +- app/views/maps/all_npo_supporters.rabl | 6 +- app/views/maps/all_npos.rabl | 6 +- app/views/maps/all_supporters.rabl | 7 +- app/views/maps/specific_npo_supporters.rabl | 6 +- .../nonprofits/custom_field_joins/index.rabl | 6 +- .../custom_field_masters/index.rabl | 8 +- app/views/nonprofits/payments/show.rabl | 46 +- .../nonprofits/recurring_donations/show.rabl | 8 +- app/views/nonprofits/refunds/index.rabl | 7 +- bin/bundle | 4 +- bin/rails | 2 + bin/rake | 2 + bin/setup | 4 +- bin/update | 4 +- config.ru | 4 +- config/application.rb | 123 +-- config/boot.rb | 4 +- config/environment.rb | 76 +- config/environments/ci.rb | 6 +- config/environments/development.rb | 7 +- config/environments/production.rb | 7 +- config/environments/staging.rb | 107 +-- config/environments/test.rb | 4 +- .../application_controller_renderer.rb | 2 + config/initializers/assets.rb | 2 + config/initializers/aws.rb | 6 +- config/initializers/aws_ses.rb | 8 +- config/initializers/backtrace_silencers.rb | 2 + config/initializers/block_ips.rb | 5 +- config/initializers/carrierwave.rb | 22 +- config/initializers/chunked_uploader.rb | 4 +- config/initializers/config.rb | 2 + config/initializers/cookies_serializer.rb | 2 + config/initializers/delayed_job_config.rb | 2 + config/initializers/devise.rb | 368 ++++----- config/initializers/devise_async.rb | 4 +- config/initializers/email_jobs.rb | 4 +- .../initializers/filter_parameter_logging.rb | 2 + config/initializers/fullcontact.rb | 8 +- config/initializers/geocode.rb | 6 +- config/initializers/hamster_extensions.rb | 4 +- config/initializers/inflections.rb | 2 + config/initializers/locale.rb | 2 + config/initializers/log_rage.rb | 22 +- config/initializers/mailchimp.rb | 8 +- config/initializers/mime_types.rb | 2 + config/initializers/pg_type_map.rb | 5 +- config/initializers/quiet_assets.rb | 2 + config/initializers/rabl_init.rb | 4 +- config/initializers/reload_api.rb | 2 + config/initializers/secret_token.rb | 4 +- config/initializers/session_store.rb | 2 + config/initializers/slack_notice.rb | 4 +- config/initializers/stripe.rb | 4 +- config/initializers/time.rb | 2 + config/initializers/timeout.rb | 4 +- config/initializers/wrap_parameters.rb | 2 + config/puma.rb | 11 +- config/routes.rb | 440 ++++++----- config/spring.rb | 6 +- ...dexes_for_payment_and_supporter_queries.rb | 42 +- db/migrate/20170307223525_drop_all_cruft.rb | 63 +- ..._end_datetimes_for_events_and_campaigns.rb | 11 +- ...22203228_add_donation_campaign_id_index.rb | 13 +- ...05180556_add_the_tag_joins_backup_table.rb | 2 + ..._index_for_tag_joins_and_add_constraint.rb | 6 +- ...558_add_custom_field_joins_backup_table.rb | 2 + ...ex_for_custom_field_join_and_supporters.rb | 6 +- .../20170808180559_add_inactive_to_card.rb | 6 +- db/migrate/20170818201127_create_exports.rb | 2 + ...002160808_create_miscellaneous_np_infos.rb | 2 + ...0171002164402_add_currency_to_nonprofit.rb | 2 + ...mount_message_to_miscellaneous_np_infos.rb | 2 + ...37_add_first_and_last_name_to_supporter.rb | 2 + db/migrate/20171016181942_add_tracking.rb | 2 + ...06_add_queued_for_import_at_to_donation.rb | 2 + .../20171026102139_add_direct_debit_detail.rb | 10 +- ...1129215957_add_utm_content_to_trackings.rb | 2 + ...20171130182254_add_locale_to_supporters.rb | 2 + ...93955_add_payment_provider_to_donations.rb | 2 + ...3317_add_direct_debit_detail_to_charges.rb | 2 + ...229_add_external_identifier_to_campaign.rb | 2 + ...71207191435_add_index_to_campaign_gifts.rb | 2 + .../20171207191712_add_index_to_activities.rb | 2 + ...1207200746_modify_supporters_name_index.rb | 2 + ...0171207200950_add_supporters_name_index.rb | 2 + ...1207210431_add_charges_payment_id_index.rb | 2 + ...ndexes_for_supporter_deleted_and_import.rb | 2 + .../20180119215653_create_payment_imports.rb | 2 + ...te_donations_payment_imports_join_table.rb | 2 + .../20180202181929_remove_unused_metadata.rb | 74 +- ...1755_remove_recurring_donation_event_id.rb | 3 +- .../20180214124311_create_source_tokens.rb | 11 +- ...20180215124311_add_card_token_to_ticket.rb | 2 + ...16064311_add_indexes_to_supporter_notes.rb | 4 +- ...16124311_change_ddd_supporter_to_holder.rb | 2 + db/migrate/20180217124311_remove_articles.rb | 5 +- ...49_add_index_to_supporter_id_on_tickets.rb | 2 + ...dex_to_event_id_on_donations_and_events.rb | 2 + ...401_add_parent_campaign_id_to_campaigns.rb | 2 + ..._add_reason_for_supporting_to_campaigns.rb | 2 + ...ault_reason_for_supporting_to_campaigns.rb | 2 + ...703165405_add_banner_image_to_campaigns.rb | 2 + .../20180713213748_add_charge_id_indexes.rb | 3 +- ...0180713215825_add_payment_id_to_tickets.rb | 2 + .../20180713220028_add_indexes_to_refunds.rb | 2 + .../20181002160627_correct_dedications.rb | 34 +- ...81003212559_correct_dedication_contacts.rb | 59 +- ...dd_index_parent_campaign_id_to_campaign.rb | 2 + ...1143_add_indexes_to_recurring_donations.rb | 2 + ...add_donation_id_index_to_campaign_gifts.rb | 2 + ...224030_add_index_to_payments_created_at.rb | 2 + db/seeds.rb | 6 +- gems/grape_devise/Gemfile | 4 +- gems/grape_devise/Rakefile | 4 +- gems/grape_devise/grape_devise.gemspec | 44 +- gems/grape_devise/lib/grape_devise.rb | 8 +- gems/grape_devise/lib/grape_devise/api.rb | 7 +- gems/grape_devise/lib/grape_devise/version.rb | 4 +- gems/grape_devise/spec/dummy/Rakefile | 4 +- gems/grape_devise/spec/dummy/app/api/api.rb | 12 +- .../app/controllers/application_controller.rb | 2 + .../dummy/app/helpers/application_helper.rb | 2 + .../spec/dummy/app/models/user.rb | 2 + gems/grape_devise/spec/dummy/bin/bundle | 4 +- gems/grape_devise/spec/dummy/bin/rails | 4 +- gems/grape_devise/spec/dummy/bin/rake | 2 + gems/grape_devise/spec/dummy/config.ru | 4 +- .../spec/dummy/config/application.rb | 7 +- gems/grape_devise/spec/dummy/config/boot.rb | 10 +- .../spec/dummy/config/environment.rb | 4 +- .../dummy/config/environments/development.rb | 2 + .../dummy/config/environments/production.rb | 2 + .../spec/dummy/config/environments/test.rb | 4 +- .../initializers/backtrace_silencers.rb | 2 + .../spec/dummy/config/initializers/devise.rb | 6 +- .../initializers/filter_parameter_logging.rb | 2 + .../dummy/config/initializers/inflections.rb | 2 + .../dummy/config/initializers/mime_types.rb | 2 + .../dummy/config/initializers/secret_token.rb | 2 + .../config/initializers/session_store.rb | 2 + .../config/initializers/wrap_parameters.rb | 2 + gems/grape_devise/spec/dummy/config/routes.rb | 5 +- .../20140913043018_devise_create_users.rb | 7 +- gems/grape_devise/spec/dummy/db/schema.rb | 37 +- gems/grape_devise/spec/factories.rb | 8 +- gems/grape_devise/spec/requests/user_spec.rb | 37 +- gems/grape_devise/spec/spec_helper.rb | 10 +- gems/ruby-param-validation/Rakefile | 6 +- .../lib/param_validation.rb | 191 +++-- .../param_validation.gemspec | 2 + .../test/param_validation_test.rb | 138 ++-- gems/ruby-qx/lib/qx.rb | 31 +- gems/ruby-qx/qx.gemspec | 4 +- gems/ruby-qx/test/UpsertTest.rb | 16 +- gems/ruby-qx/test/qx_test.rb | 238 +++--- lib/audit.rb | 57 +- lib/calculate/calculate_fees.rb | 13 +- lib/calculate/calculate_suggested_amounts.rb | 52 +- lib/cancel_billing_subscription.rb | 28 +- lib/chunked_uploader/s3.rb | 18 +- .../construct_billing_subscription.rb | 31 +- lib/construct/construct_nonprofit.rb | 23 +- lib/controllers/campaign_helper.rb | 32 +- lib/controllers/event_helper.rb | 38 +- lib/controllers/nonprofit_helper.rb | 90 +-- lib/copy_naming_algorithm.rb | 37 +- lib/create/create_campaign.rb | 15 +- lib/create/create_campaign_gift.rb | 135 ++-- lib/create/create_campaign_gift_option.rb | 14 +- lib/create/create_custom_field_join.rb | 63 +- lib/create/create_custom_field_master.rb | 11 +- lib/create/create_peer_to_peer_campaign.rb | 25 +- lib/create/create_tag_master.rb | 12 +- lib/create/stripe/create_stripe_account.rb | 61 +- lib/cypher.rb | 13 +- lib/delayed_job_helper.rb | 19 +- lib/delete/delete_campaign_gift_option.rb | 33 +- lib/delete/delete_custom_field_joins.rb | 38 +- lib/delete/delete_tag_joins.rb | 41 +- lib/email.rb | 8 +- lib/email_job_queue.rb | 4 +- lib/errors/authentication_error.rb | 4 +- lib/errors/cc_org_error.rb | 4 +- lib/errors/charge_error.rb | 4 +- lib/errors/expired_token_error.rb | 4 +- lib/errors/not_enough_quantity_error.rb | 4 +- lib/export/export_payments.rb | 18 +- lib/export/export_recurring_donations.rb | 14 +- lib/export/export_supporter_notes.rb | 143 ++-- lib/export/export_supporters.rb | 21 +- lib/fetch/fetch_background_image.rb | 9 +- lib/fetch/fetch_campaign.rb | 21 +- lib/fetch/fetch_coupon.rb | 10 +- lib/fetch/fetch_event.rb | 21 +- lib/fetch/fetch_miscellaneous_np_info.rb | 9 +- lib/fetch/fetch_nonprofit.rb | 19 +- lib/fetch/fetch_nonprofit_email.rb | 19 +- lib/fetch/fetch_todo_status.rb | 49 +- lib/fetch/stripe/fetch_stripe_account.rb | 20 +- lib/format/format/address.rb | 35 +- lib/format/format/csv.rb | 22 +- lib/format/format/currency.rb | 36 +- lib/format/format/date.rb | 111 +-- lib/format/format/dedication.rb | 7 +- lib/format/format/geography.rb | 600 +++++++-------- lib/format/format/html.rb | 4 +- lib/format/format/indefinitize.rb | 20 +- lib/format/format/interpolate.rb | 5 +- lib/format/format/name.rb | 18 +- lib/format/format/phone.rb | 38 +- lib/format/format/remove_diacritics.rb | 21 +- lib/format/format/timezone.rb | 27 +- lib/format/format/url.rb | 46 +- lib/generators/api/entity/entity_generator.rb | 6 +- .../api/resource/resource_generator.rb | 10 +- .../api/validator/validator_generator.rb | 12 +- .../email_job/email_job_generator.rb | 6 +- .../libmodule/libmodule_generator.rb | 6 +- .../react/component/component_generator.rb | 8 +- lib/generators/react/lib/lib_generator.rb | 8 +- .../react/packroot/packroot_generator.rb | 5 +- .../ts/declaration/declaration_generator.rb | 6 +- lib/geocode_model.rb | 73 +- lib/get_data.rb | 52 +- lib/hash.rb | 10 +- lib/health_report.rb | 37 +- lib/htp.rb | 2 + lib/image.rb | 17 +- lib/import/import_civicrm_payments.rb | 146 ++-- lib/include_asset.rb | 11 +- lib/insert/insert_activities.rb | 209 +++-- lib/insert/insert_bank_account.rb | 90 ++- lib/insert/insert_billing_subscriptions.rb | 26 +- lib/insert/insert_card.rb | 81 +- lib/insert/insert_charge.rb | 268 ++++--- lib/insert/insert_custom_field_joins.rb | 133 ++-- lib/insert/insert_direct_debit_detail.rb | 10 +- lib/insert/insert_disputes.rb | 45 +- lib/insert/insert_donation.rb | 123 +-- lib/insert/insert_duplicate.rb | 134 ++-- lib/insert/insert_email_lists.rb | 35 +- lib/insert/insert_full_contact_infos.rb | 193 +++-- lib/insert/insert_import.rb | 84 +- lib/insert/insert_nonprofit_keys.rb | 44 +- lib/insert/insert_payout.rb | 113 +-- lib/insert/insert_recurring_donation.rb | 117 ++- lib/insert/insert_refunds.rb | 60 +- lib/insert/insert_source_token.rb | 22 +- lib/insert/insert_supporter.rb | 86 +-- lib/insert/insert_supporter_notes.rb | 27 +- lib/insert/insert_tag_joins.rb | 102 ++- lib/insert/insert_tickets.rb | 117 +-- lib/insert/insert_tracking.rb | 22 +- lib/job_types/admin_failed_gift_job.rb | 4 +- lib/job_types/admin_notice_job.rb | 4 +- .../campaign_creation_followup_job.rb | 4 +- .../donor_direct_debit_notification_job.rb | 6 +- .../donor_failed_recurring_donation_job.rb | 4 +- .../donor_payment_notification_job.rb | 24 +- ...or_recurring_donation_change_amount_job.rb | 24 +- .../donor_refund_notification_job.rb | 22 +- lib/job_types/email_job.rb | 41 +- lib/job_types/event_creation_followup_job.rb | 4 +- lib/job_types/export_payment_completed_job.rb | 4 +- lib/job_types/export_payment_failed_job.rb | 4 +- ...xport_recurring_donations_completed_job.rb | 4 +- .../export_recurring_donations_failed_job.rb | 4 +- .../export_supporter_notes_completed_job.rb | 4 +- .../export_supporter_notes_failed_job.rb | 4 +- .../export_supporters_completed_job.rb | 4 +- lib/job_types/export_supporters_failed_job.rb | 4 +- lib/job_types/generic_mail_job.rb | 4 +- .../import_complete_notification_job.rb | 4 +- .../nonprofit_admin_existing_invite_job.rb | 4 +- .../nonprofit_admin_new_invite_job.rb | 4 +- ...onprofit_admin_supporter_fundraiser_job.rb | 4 +- ...nonprofit_failed_recurring_donation_job.rb | 4 +- .../nonprofit_failed_verification_job.rb | 4 +- .../nonprofit_new_bank_account_job.rb | 4 +- .../nonprofit_payment_notification_job.rb | 24 +- lib/job_types/nonprofit_pending_payout_job.rb | 4 +- ...fit_recurring_donation_cancellation_job.rb | 4 +- ...it_recurring_donation_change_amount_job.rb | 6 +- .../nonprofit_refund_notification_job.rb | 22 +- .../nonprofit_successful_verification_job.rb | 4 +- lib/job_types/nonprofit_welcome_job.rb | 4 +- lib/job_types/ticket_mailer_followup_job.rb | 4 +- .../ticket_mailer_receipt_admin_job.rb | 6 +- lib/json_resp.rb | 82 +- lib/list/list_activities.rb | 26 +- lib/mailchimp.rb | 232 +++--- lib/maintain/maintain_dedications.rb | 42 +- lib/maintain/maintain_payment_records.rb | 10 +- lib/maintain/maintain_ticket_records.rb | 13 +- lib/merge_supporters.rb | 81 +- lib/metrics/nonprofit_metrics.rb | 188 ++--- lib/name_copy_naming_algorithm.rb | 20 +- lib/notify/notify_user.rb | 11 +- lib/numeric.rb | 14 +- lib/onboard_accounts.rb | 127 ++- lib/parallel_ar.rb | 6 +- lib/path/nonprofit_path.rb | 16 +- lib/pay_recurring_donation.rb | 129 ++-- lib/psql.rb | 10 +- lib/qexpr.rb | 132 ++-- lib/qexpr_query_chunker.rb | 43 +- lib/query/billing_plans.rb | 23 +- lib/query/query_activities.rb | 5 +- lib/query/query_billing_subscriptions.rb | 18 +- lib/query/query_campaign_gifts.rb | 48 +- lib/query/query_campaign_metrics.rb | 30 +- lib/query/query_campaigns.rb | 78 +- lib/query/query_charges.rb | 2 + lib/query/query_custom_fields.rb | 15 +- lib/query/query_donations.rb | 105 ++- lib/query/query_email_settings.rb | 17 +- lib/query/query_event_discounts.rb | 16 +- lib/query/query_event_metrics.rb | 84 +- lib/query/query_event_organizer.rb | 20 +- lib/query/query_events.rb | 14 +- lib/query/query_full_contact_infos.rb | 13 +- lib/query/query_nonprofit_keys.rb | 15 +- lib/query/query_nonprofits.rb | 114 +-- lib/query/query_payments.rb | 367 +++++---- lib/query/query_profiles.rb | 40 +- lib/query/query_recurring_donations.rb | 280 ++++--- lib/query/query_roles.rb | 45 +- lib/query/query_source_token.rb | 35 +- lib/query/query_supporters.rb | 602 +++++++-------- lib/query/query_ticket_levels.rb | 52 +- lib/query/query_tickets.rb | 144 ++-- lib/query/query_users.rb | 33 +- lib/queue_donations.rb | 52 +- lib/qx_query_chunker.rb | 4 +- lib/required_keys.rb | 7 +- lib/retrieve/retrieve_active_record_items.rb | 48 +- lib/scheduled_jobs.rb | 57 +- lib/search_vector.rb | 32 +- lib/slug_copy_naming_algorithm.rb | 14 +- lib/slug_nonprofit_naming_algorithm.rb | 20 +- lib/slug_p2p_campaign_naming_algorithm.rb | 12 +- lib/stripe_account.rb | 65 +- lib/stripe_utils.rb | 52 +- lib/tasks/civicrm.rake | 8 +- lib/tasks/database.rake | 20 +- lib/tasks/full_contact.rake | 11 +- lib/tasks/health_report.rake | 10 +- lib/tasks/oapi.rake | 6 +- lib/tasks/scheduler.rake | 22 +- lib/tasks/seed.rake | 27 +- lib/tasks/settings.rake | 23 +- lib/timespan.rb | 55 +- lib/update/update_activities.rb | 18 +- lib/update/update_billing_subscriptions.rb | 3 +- lib/update/update_campaign_gift_option.rb | 13 +- lib/update/update_charges.rb | 15 +- lib/update/update_custom_field_joins.rb | 19 +- lib/update/update_disputes.rb | 5 +- lib/update/update_donation.rb | 97 ++- lib/update/update_email_lists.rb | 28 +- lib/update/update_email_settings.rb | 11 +- lib/update/update_miscellaneous_np_info.rb | 21 +- lib/update/update_nonprofit.rb | 60 +- lib/update/update_order.rb | 11 +- lib/update/update_payouts.rb | 26 +- lib/update/update_recurring_donations.rb | 85 +-- lib/update/update_refunds.rb | 23 +- lib/update/update_supporter.rb | 18 +- lib/update/update_supporter_notes.rb | 11 +- lib/update/update_tickets.rb | 29 +- lib/uuid.rb | 6 +- lib/validation_error.rb | 2 + script/delayed_job | 1 + script/rails | 8 +- spec/api/houdini/nonprofit_spec.rb | 175 ++--- .../support/api_shared_user_verification.rb | 115 ++- spec/controllers/aws_presigned_posts_spec.rb | 6 +- .../controllers/billing_subscriptions_spec.rb | 14 +- .../controllers/campaign_gift_options_spec.rb | 14 +- spec/controllers/campaign_gifts_spec.rb | 6 +- .../campaigns/campaign_gift_options_spec.rb | 8 +- spec/controllers/campaigns/donations_spec.rb | 8 +- spec/controllers/campaigns/supporters_spec.rb | 6 +- spec/controllers/campaigns_spec.rb | 27 +- spec/controllers/cards_spec.rb | 8 +- spec/controllers/direct_debit_details_spec.rb | 10 +- spec/controllers/email_settings_spec.rb | 13 +- spec/controllers/emails_spec.rb | 8 +- spec/controllers/event_discounts_spec.rb | 22 +- spec/controllers/events_spec.rb | 32 +- spec/controllers/front_spec.rb | 11 +- spec/controllers/image_attachments_spec.rb | 6 +- spec/controllers/maps_spec.rb | 16 +- .../controllers/nonprofits/activities_spec.rb | 6 +- .../nonprofits/bank_accounts_spec.rb | 6 +- spec/controllers/nonprofits/button_spec.rb | 8 +- spec/controllers/nonprofits/cards_spec.rb | 6 +- spec/controllers/nonprofits/charges_spec.rb | 6 +- .../nonprofits/custom_field_masters_spec.rb | 6 +- .../nonprofits/custom_fields_joins_spec.rb | 10 +- spec/controllers/nonprofits/donations_spec.rb | 21 +- .../nonprofits/email_lists_spec.rb | 6 +- spec/controllers/nonprofits/imports_spec.rb | 6 +- .../nonprofits/miscellaneous_np_infos_spec.rb | 6 +- .../nonprofits/nonprofit_keys_spec.rb | 6 +- spec/controllers/nonprofits/payments_spec.rb | 6 +- spec/controllers/nonprofits/payouts_spec.rb | 8 +- .../nonprofits/recurring_donations_spec.rb | 8 +- spec/controllers/nonprofits/reports_spec.rb | 6 +- .../nonprofits/supporter_emails_spec.rb | 6 +- .../controllers/nonprofits/supporters_spec.rb | 6 +- spec/controllers/nonprofits/tag_joins_spec.rb | 18 +- .../nonprofits/tag_masters_spec.rb | 12 +- spec/controllers/nonprofits/trackings_spec.rb | 8 +- spec/controllers/nonprofits_spec.rb | 38 +- spec/controllers/onboard_controller_spec.rb | 5 +- spec/controllers/profiles_spec.rb | 12 +- spec/controllers/recurring_donations_spec.rb | 6 +- spec/controllers/roles_spec.rb | 14 +- spec/controllers/settings_spec.rb | 6 +- spec/controllers/static_controller_spec.rb | 36 +- spec/controllers/super_admins_spec.rb | 16 +- .../support/new_controller_user_context.rb | 23 +- .../support/shared_user_context.rb | 171 ++--- spec/controllers/ticket_levels_spec.rb | 19 +- spec/controllers/tickets_spec.rb | 16 +- spec/cve/cve_2014_2538_spec.rb | 8 +- spec/cve/cve_2015_3225_spec.rb | 9 +- spec/cve/cve_2015_3226_spec.rb | 15 +- spec/factories/bank_accounts.rb | 3 +- spec/factories/billing_plans.rb | 3 +- spec/factories/billing_subscriptions.rb | 3 +- spec/factories/campaign_gift_options.rb | 4 +- spec/factories/campaign_gifts.rb | 3 +- spec/factories/campaigns.rb | 6 +- spec/factories/cards.rb | 26 +- spec/factories/charges.rb | 3 +- spec/factories/custom_field_joins.rb | 3 +- spec/factories/custom_field_masters.rb | 6 +- spec/factories/direct_debit_details.rb | 3 +- spec/factories/disputes.rb | 3 +- spec/factories/donations.rb | 3 +- spec/factories/email_lists.rb | 3 +- spec/factories/event_discounts.rb | 2 + spec/factories/events.rb | 16 +- spec/factories/exports.rb | 2 + spec/factories/miscellaneous_np_infos.rb | 3 +- spec/factories/nonprofits.rb | 23 +- spec/factories/offsite_payments.rb | 3 +- spec/factories/payment_imports.rb | 6 +- spec/factories/payment_payouts.rb | 3 +- spec/factories/payments.rb | 3 +- spec/factories/payouts.rb | 3 +- spec/factories/profiles.rb | 4 +- spec/factories/recurring_donations.rb | 3 +- spec/factories/refunds.rb | 3 +- spec/factories/roles.rb | 3 +- spec/factories/source_tokens.rb | 2 + spec/factories/supporter_notes.rb | 3 +- spec/factories/supporters.rb | 8 +- spec/factories/tag_joins.rb | 2 + spec/factories/tag_masters.rb | 3 +- spec/factories/ticket_levels.rb | 3 +- spec/factories/tickets.rb | 3 +- spec/factories/users.rb | 6 +- spec/lib/calculate/calculate_fees_spec.rb | 33 +- .../calculate_suggested_amounts_spec.rb | 43 +- spec/lib/cancel_billing_subscriptions_spec.rb | 59 +- spec/lib/chunked_uploader/s3_spec.rb | 2 + .../lib/construct/construct_nonprofit_spec.rb | 31 +- spec/lib/copy_naming_algorithm_spec.rb | 89 +-- spec/lib/create/create_campaign_gift_spec.rb | 199 +++-- spec/lib/create/create_campaign_spec.rb | 6 +- .../create_peer_to_peer_campaign_spec.rb | 12 +- .../delete_campaign_gift_option_spec.rb | 84 +- spec/lib/email_job_queue_spec.rb | 9 +- spec/lib/export/export_payments_spec.rb | 47 +- .../export/export_recurring_donations_spec.rb | 34 +- .../lib/export/export_supporter_notes_spec.rb | 49 +- spec/lib/export/export_supporters_spec.rb | 30 +- spec/lib/fetch/fetch_coupon_spec.rb | 15 +- .../fetch_misc_nonprofit_settings_spec.rb | 21 +- spec/lib/fetch/fetch_nonprofit_email_spec.rb | 34 +- spec/lib/format/currency_spec.rb | 72 +- spec/lib/format/dedication_spec.rb | 12 +- spec/lib/format/geography_spec.rb | 28 +- spec/lib/format/indefinitize_spec.rb | 10 +- spec/lib/format/name_spec.rb | 8 +- spec/lib/format/url_spec.rb | 21 +- .../import/import_civicrm_payments_spec.rb | 6 +- spec/lib/insert/insert_bank_account_spec.rb | 155 ++-- .../insert_billing_subscriptions_spec.rb | 14 +- spec/lib/insert/insert_card_spec.rb | 653 ++++++++-------- spec/lib/insert/insert_charge_spec.rb | 720 +++++++++--------- .../insert/insert_custom_field_joins_spec.rb | 288 ++++--- spec/lib/insert/insert_disputes_spec.rb | 14 +- spec/lib/insert/insert_donation_spec.rb | 110 ++- spec/lib/insert/insert_duplicate_spec.rb | 476 ++++++------ spec/lib/insert/insert_import_spec.rb | 52 +- spec/lib/insert/insert_payout_spec.rb | 352 +++++---- .../insert/insert_recurring_donation_spec.rb | 232 +++--- spec/lib/insert/insert_refunds_spec.rb | 91 ++- spec/lib/insert/insert_source_token_spec.rb | 120 ++- spec/lib/insert/insert_tag_joins_spec.rb | 256 +++---- spec/lib/insert/insert_tickets_spec.rb | 495 ++++++------ .../job_types/admin_failed_gift_job_spec.rb | 8 +- spec/lib/job_types/admin_notice_job_spec.rb | 6 +- .../campaign_creation_followup_job_spec.rb | 6 +- ...onor_direct_debit_notification_job_spec.rb | 6 +- ...onor_failed_recurring_donation_job_spec.rb | 6 +- .../donor_payment_notification_job_spec.rb | 6 +- ...curring_donation_change_amount_job_spec.rb | 6 +- .../donor_refund_notification_job_spec.rb | 5 +- spec/lib/job_types/email_job_spec.rb | 8 +- .../event_creation_followup_job_spec.rb | 6 +- .../export_payment_completed_job_spec.rb | 6 +- .../export_payment_failed_job_spec.rb | 6 +- ..._recurring_donations_completed_job_spec.rb | 6 +- ...ort_recurring_donations_failed_job_spec.rb | 6 +- .../export_supporter_notes_completed_spec.rb | 6 +- .../export_supporter_notes_failed_spec.rb | 8 +- .../export_supporters_completed_job_spec.rb | 6 +- .../export_supporters_failed_job_spec.rb | 6 +- spec/lib/job_types/generic_mail_job_spec.rb | 8 +- .../import_complete_notification_job_spec.rb | 6 +- ...onprofit_admin_existing_invite_job_spec.rb | 6 +- .../nonprofit_admin_new_invite_job_spec.rb | 8 +- ...fit_admin_supporter_fundraiser_job_spec.rb | 6 +- ...ofit_failed_recurring_donation_job_spec.rb | 6 +- .../nonprofit_failed_verification_job_spec.rb | 6 +- .../nonprofit_new_bank_account_job_spec.rb | 6 +- ...nonprofit_payment_notification_job_spec.rb | 6 +- .../nonprofit_pending_payout_job_spec.rb | 6 +- ...ecurring_donation_cancellation_job_spec.rb | 6 +- ...curring_donation_change_amount_job_spec.rb | 6 +- .../nonprofit_refund_notification_job_spec.rb | 5 +- ...profit_successful_verification_job_spec.rb | 6 +- .../job_types/nonprofit_welcome_job_spec.rb | 6 +- .../ticket_mailer_followup_job_spec.rb | 6 +- .../ticket_mailer_receipt_admin_job_spec.rb | 6 +- spec/lib/mailchimp_spec.rb | 69 +- spec/lib/merge_supporters_spec.rb | 83 +- spec/lib/name_copy_naming_algorithm_spec.rb | 68 +- spec/lib/numeric_spec.rb | 121 ++- spec/lib/pay_recurring_donation_spec.rb | 21 +- spec/lib/query/billing_plans_spec.rb | 31 +- .../query/query_billing_subscriptions_spec.rb | 42 +- spec/lib/query/query_campaign_gifts_spec.rb | 58 +- spec/lib/query/query_campaign_metrics_spec.rb | 33 +- spec/lib/query/query_donations_spec.rb | 65 +- spec/lib/query/query_payments_spec.rb | 323 ++++---- .../query/query_recurring_donations_spec.rb | 122 ++- spec/lib/query/query_roles_spec.rb | 13 +- spec/lib/query/query_source_token_spec.rb | 86 ++- spec/lib/query/query_supporters_spec.rb | 65 +- spec/lib/query/query_ticket_levels_spec.rb | 70 +- spec/lib/query/query_users_spec.rb | 27 +- .../retrieve_active_record_items_spec.rb | 57 +- spec/lib/slug_copy_naming_algorithm_spec.rb | 63 +- .../slug_nonprofit_naming_algorithm_spec.rb | 54 +- ...slug_p2p_campaign_naming_algorithm_spec.rb | 42 +- spec/lib/stripe_account_spec.rb | 114 ++- spec/lib/stripe_utils_spec.rb | 28 +- spec/lib/timespan_spec.rb | 106 ++- spec/lib/update/update_charges_spec.rb | 27 +- spec/lib/update/update_disputes_spec.rb | 6 +- spec/lib/update/update_donation_spec.rb | 372 +++++---- .../update_misc_nonprofit_settings_spec.rb | 47 +- spec/lib/update/update_payouts_spec.rb | 73 +- .../update/update_recurring_donations_spec.rb | 118 ++- spec/lib/update/update_refunds_spec.rb | 25 +- spec/lib/update/update_supporter_spec.rb | 6 +- spec/lib/update/update_tickets_spec.rb | 205 +++-- spec/lib/uuid_spec.rb | 2 + spec/mailers/admin_spec.rb | 47 +- spec/mailers/donation_mailer_spec.rb | 66 +- .../delete_custom_field_join_spec.rb | 78 +- spec/migration/delete_tag_join_spec.rb | 77 +- spec/migration/migration_sanity_spec.rb | 22 +- spec/models/campaign_spec.rb | 14 +- spec/models/nonprofit_spec.rb | 16 +- spec/models/payment_import_spec.rb | 4 +- spec/models/ticket_spec.rb | 23 +- spec/rails_helper.rb | 12 +- spec/requests/maintenance_spec.rb | 51 +- .../nonprofits/direct_debit_details_spec.rb | 25 +- spec/spec_helper.rb | 110 ++- spec/support/construct.rb | 20 +- spec/support/contexts.rb | 4 +- .../contexts/general_shared_user_context.rb | 104 ++- .../shared_donation_charge_context.rb | 65 +- .../shared_rd_donation_value_context.rb | 407 +++++----- spec/support/date_time.rb | 4 +- spec/support/expect.rb | 23 +- spec/support/factory_bot.rb | 16 +- spec/support/init_dotenv.rb | 4 +- spec/support/mock_helpers.rb | 24 +- spec/support/payments_for_a_payout.rb | 92 +-- spec/support/test_chunked_uploader.rb | 14 +- 759 files changed, 14563 insertions(+), 14380 deletions(-) diff --git a/Gemfile b/Gemfile index 0a5dda0f..5039b1d3 100755 --- a/Gemfile +++ b/Gemfile @@ -1,14 +1,16 @@ +# frozen_string_literal: true + source 'https://rubygems.org' ruby '2.5.1' -gem 'rake' gem 'rails', '= 5.2.3' +gem 'rake' # https://stripe.com/docs/api gem 'stripe' # Compression of assets on heroku # https://github.com/romanbsd/heroku-deflater -gem 'heroku-deflater', :group => :production +gem 'heroku-deflater', group: :production # json serialization # https://github.com/nesquena/rabl @@ -16,17 +18,17 @@ gem 'rabl' gem 'parallel' -gem 'puma' gem 'bootsnap', require: false -gem 'rack-timeout' -gem 'rack-ssl' +gem 'puma' gem 'puma_worker_killer' +gem 'rack-ssl' +gem 'rack-timeout' -gem 'test-unit', '~> 3.0' gem 'hamster' +gem 'test-unit', '~> 3.0' -gem 'aws-ses' gem 'aws-sdk', '~> 1' +gem 'aws-ses' # for blocking ip addressses gem 'rack-attack' @@ -44,7 +46,6 @@ gem 'qx', path: 'gems/ruby-qx' gem 'dalli' gem 'memcachier' - gem 'param_validation', path: 'gems/ruby-param-validation' # Print colorized text lol @@ -89,33 +90,32 @@ gem 'table_print' gem 'bunny', '>= 2.6.3' -gem 'rails-i18n' -gem 'i18n-js' gem 'countries' - +gem 'i18n-js' +gem 'rails-i18n' group :development, :ci do - gem 'traceroute' gem 'debase' gem 'ruby-debug-ide' + gem 'traceroute' end group :development, :ci, :test do - gem 'timecop' - gem 'pry' - #gem 'pry-byebug' - gem 'binding_of_caller' - gem 'rspec' - gem 'rspec-rails' - gem 'database_cleaner' + gem 'pry' + gem 'timecop' + # gem 'pry-byebug' + gem 'action_mailer_matchers' + gem 'binding_of_caller' + gem 'database_cleaner' gem 'dotenv-rails' - gem 'ruby-prof', '0.15.9' - gem 'stripe-ruby-mock', '~> 2.4.1', :require => 'stripe_mock', git: 'https://github.com/commitchange/stripe-ruby-mock.git', :branch => '2.4.1' gem 'factory_bot' - gem 'factory_bot_rails' - gem 'action_mailer_matchers' + gem 'factory_bot_rails' + gem 'rspec' + gem 'rspec-rails' + gem 'ruby-prof', '0.15.9' gem 'simplecov', '~> 0.16.1', require: false gem 'solargraph' + gem 'stripe-ruby-mock', '~> 2.4.1', require: 'stripe_mock', git: 'https://github.com/commitchange/stripe-ruby-mock.git', branch: '2.4.1' end group :test do @@ -139,6 +139,6 @@ gem 'grape' gem 'grape-entity' gem 'grape-swagger' gem 'grape-swagger-entity' -gem 'grape_url_validator' -gem 'grape_logging' gem 'grape_devise', path: 'gems/grape_devise' +gem 'grape_logging' +gem 'grape_url_validator' diff --git a/Rakefile b/Rakefile index c099dee5..9a148353 100755 --- a/Rakefile +++ b/Rakefile @@ -1,7 +1,9 @@ #!/usr/bin/env rake +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) Commitchange::Application.load_tasks diff --git a/app/api/houdini/api.rb b/app/api/houdini/api.rb index b2c54ab2..f1e5f35c 100644 --- a/app/api/houdini/api.rb +++ b/app/api/houdini/api.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::API < Grape::API format :json mount Houdini::V1::API => '/v1' -end \ No newline at end of file +end diff --git a/app/api/houdini/v1/api.rb b/app/api/houdini/v1/api.rb index d0e3a921..43b6516b 100644 --- a/app/api/houdini/v1/api.rb +++ b/app/api/houdini/v1/api.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'houdini/v1/validations' class Houdini::V1::API < Grape::API - logger.formatter = GrapeLogging::Formatters::Rails.new - use GrapeLogging::Middleware::RequestLogger, { logger: logger } - content_type :json, 'application/json' - default_format :json - rescue_from Grape::Exceptions::ValidationErrors do |e| - output = {errors: e} - error! output, 400 - end + logger.formatter = GrapeLogging::Formatters::Rails.new + use GrapeLogging::Middleware::RequestLogger, logger: logger + content_type :json, 'application/json' + default_format :json + rescue_from Grape::Exceptions::ValidationErrors do |e| + output = { errors: e } + error! output, 400 + end - #include Houdini::V1::Helpers::ApplicationHelper - mount Houdini::V1::Nonprofit => '/nonprofit' - # Additional mounts are added via generators above this line + # include Houdini::V1::Helpers::ApplicationHelper + mount Houdini::V1::Nonprofit => '/nonprofit' + # Additional mounts are added via generators above this line # DON'T REMOVE THIS OR THE PREVIOUS LINES!!! - uri_for_host = URI.parse(Settings.api_domain&.url || Settings.cdn.url) - add_swagger_documentation \ - host: "#{uri_for_host.host}#{uri_for_host.port ? ":#{uri_for_host.port}" : ""}", - schemes: [uri_for_host.scheme], - base_path: '/api/v1' -end \ No newline at end of file + uri_for_host = URI.parse(Settings.api_domain&.url || Settings.cdn.url) + add_swagger_documentation \ + host: "#{uri_for_host.host}#{uri_for_host.port ? ":#{uri_for_host.port}" : ''}", + schemes: [uri_for_host.scheme], + base_path: '/api/v1' +end diff --git a/app/api/houdini/v1/base_api.rb b/app/api/houdini/v1/base_api.rb index 17a5dd34..4b25d92e 100644 --- a/app/api/houdini/v1/base_api.rb +++ b/app/api/houdini/v1/base_api.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::BaseAPI < Grape::API - #helpers ApplicationHelper + # helpers ApplicationHelper # helpers do # def session # env['rack.session'] @@ -27,4 +29,4 @@ class Houdini::V1::BaseAPI < Grape::API # allow_forgery_protection.nil? || allow_forgery_protection # end # end -end \ No newline at end of file +end diff --git a/app/api/houdini/v1/entities/nonprofit.rb b/app/api/houdini/v1/entities/nonprofit.rb index 55620d7e..6922e0d0 100644 --- a/app/api/houdini/v1/entities/nonprofit.rb +++ b/app/api/houdini/v1/entities/nonprofit.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::Entities::Nonprofit < Grape::Entity expose :id -end \ No newline at end of file +end diff --git a/app/api/houdini/v1/entities/validation_error.rb b/app/api/houdini/v1/entities/validation_error.rb index af9d7342..d275a2f7 100644 --- a/app/api/houdini/v1/entities/validation_error.rb +++ b/app/api/houdini/v1/entities/validation_error.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::Entities::ValidationError < Grape::Entity - expose :params, documentation: {type: 'String', desc: 'Params where the following had an error.', is_array: true} - expose :messages, documentation: {type:'String', desc: 'The validation messages for the params', is_array: true} -end \ No newline at end of file + expose :params, documentation: { type: 'String', desc: 'Params where the following had an error.', is_array: true } + expose :messages, documentation: { type: 'String', desc: 'The validation messages for the params', is_array: true } +end diff --git a/app/api/houdini/v1/entities/validation_errors.rb b/app/api/houdini/v1/entities/validation_errors.rb index 07f03c73..303893b5 100644 --- a/app/api/houdini/v1/entities/validation_errors.rb +++ b/app/api/houdini/v1/entities/validation_errors.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::Entities::ValidationErrors < Grape::Entity - expose :errors, documentation: {type: ValidationError, desc: 'errors', is_array:true} -end \ No newline at end of file + expose :errors, documentation: { type: ValidationError, desc: 'errors', is_array: true } +end diff --git a/app/api/houdini/v1/helpers/application_helper.rb b/app/api/houdini/v1/helpers/application_helper.rb index 0c1345d6..0af8a17d 100644 --- a/app/api/houdini/v1/helpers/application_helper.rb +++ b/app/api/houdini/v1/helpers/application_helper.rb @@ -1,22 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Houdini::V1::Helpers::ApplicationHelper extend Grape::API::Helpers - def session - env['rack.session'] + env['rack.session'] end def protect_against_forgery - unless verified_request? - error!('Unauthorized', 401) - end + error!('Unauthorized', 401) unless verified_request? end def verified_request? !protect_against_forgery? || request.get? || request.head? || - form_authenticity_token == request.headers['X-CSRF-Token'] || - form_authenticity_token == request.headers['X-Csrf-Token'] + form_authenticity_token == request.headers['X-CSRF-Token'] || + form_authenticity_token == request.headers['X-Csrf-Token'] end def form_authenticity_token @@ -24,11 +23,10 @@ module Houdini::V1::Helpers::ApplicationHelper end def protect_against_forgery? - allow_forgery_protection = Rails.configuration.action_controller.allow_forgery_protection + allow_forgery_protection = Rails.configuration.action_controller.allow_forgery_protection allow_forgery_protection.nil? || allow_forgery_protection end - # def rescue_ar_invalid( *class_to_hash) # rescue_with ActiveRecord::RecordInvalid do |error| # output = [] @@ -40,6 +38,4 @@ module Houdini::V1::Helpers::ApplicationHelper # # end # end - end - diff --git a/app/api/houdini/v1/helpers/rescue_helper.rb b/app/api/houdini/v1/helpers/rescue_helper.rb index 458343e3..66de5e36 100644 --- a/app/api/houdini/v1/helpers/rescue_helper.rb +++ b/app/api/houdini/v1/helpers/rescue_helper.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true + module Houdini::V1::Helpers::RescueHelper require 'active_support/concern' extend ActiveSupport::Concern include Grape::DSL::Configuration module ClassMethods - def rescue_ar_invalid( *class_to_hash) - rescue_with ActiveRecord::RecordInvalid do |error| - output = [] - error.record.errors do |attr,message| - output.push({params: "#{class_to_hash[error.record.class]}['#{attr}']", - message: message}) + def rescue_ar_invalid(*class_to_hash) + rescue_with ActiveRecord::RecordInvalid do |error| + output = [] + error.record.errors do |attr, message| + output.push(params: "#{class_to_hash[error.record.class]}['#{attr}']", + message: message) + end + raise Grape::Exceptions::ValidationErrors, output end - raise Grape::Exceptions::ValidationErrors.new(output) - - end end end -end \ No newline at end of file +end diff --git a/app/api/houdini/v1/nonprofit.rb b/app/api/houdini/v1/nonprofit.rb index e01bc7cc..39ba746d 100644 --- a/app/api/houdini/v1/nonprofit.rb +++ b/app/api/houdini/v1/nonprofit.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI - helpers Houdini::V1::Helpers::ApplicationHelper, Houdini::V1::Helpers::RescueHelper + helpers Houdini::V1::Helpers::ApplicationHelper, Houdini::V1::Helpers::RescueHelper - before do - protect_against_forgery - end + before do + protect_against_forgery + end desc 'Return a nonprofit.' do success Houdini::V1::Entities::Nonprofit @@ -18,103 +20,92 @@ class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI present np, as: Houdini::V1::Entities::Nonprofit end end - + desc 'Register a nonprofit' do success Houdini::V1::Entities::Nonprofit - #this needs to be a validation an array - failure [{code:400, message:'Validation Errors', model: Houdini::V1::Entities::ValidationErrors}] + # this needs to be a validation an array + failure [{ code: 400, message: 'Validation Errors', model: Houdini::V1::Entities::ValidationErrors }] end params do - - requires :nonprofit, type: Hash do - requires :name, type:String, desc: 'Organization Name', allow_blank: false, documentation: { param_type: 'body' } - optional :website, type:String, desc: 'Organization website URL', allow_blank:true, regexp: URI::regexp, documentation: { param_type: 'body' }, coerce_with: ->(url) { + requires :nonprofit, type: Hash do + requires :name, type: String, desc: 'Organization Name', allow_blank: false, documentation: { param_type: 'body' } + optional :website, type: String, desc: 'Organization website URL', allow_blank: true, regexp: URI::DEFAULT_PARSER.make_regexp, documentation: { param_type: 'body' }, coerce_with: lambda { |url| coerced_url = url - unless (url =~ /\Ahttp:\/\/.*/i || url =~ /\Ahttps:\/\/.*/i) - coerced_url = 'http://'+ coerced_url + unless url =~ %r{\Ahttp://.*}i || url =~ %r{\Ahttps://.*}i + coerced_url = 'http://' + coerced_url end coerced_url } - requires :zip_code, type:String, allow_blank: false, desc: "Organization Address ZIP Code", documentation: { param_type: 'body' } - requires :state_code, type:String, allow_blank: false, desc: "Organization Address State Code", documentation: { param_type: 'body' } - requires :city, type:String, allow_blank: false, desc: "Organization Address City", documentation: { param_type: 'body' } - optional :email, type:String, desc: 'Organization email (public)', regexp: Email::Regex, documentation: { param_type: 'body' } - optional :phone, type:String, desc: 'Organization phone (public)', documentation: { param_type: 'body' } + requires :zip_code, type: String, allow_blank: false, desc: 'Organization Address ZIP Code', documentation: { param_type: 'body' } + requires :state_code, type: String, allow_blank: false, desc: 'Organization Address State Code', documentation: { param_type: 'body' } + requires :city, type: String, allow_blank: false, desc: 'Organization Address City', documentation: { param_type: 'body' } + optional :email, type: String, desc: 'Organization email (public)', regexp: Email::Regex, documentation: { param_type: 'body' } + optional :phone, type: String, desc: 'Organization phone (public)', documentation: { param_type: 'body' } end requires :user, type: Hash do - requires :name, type:String, desc: 'Full name', allow_blank:false, documentation: { param_type: 'body' } - requires :email, type:String, desc: 'Username', allow_blank: false, documentation: { param_type: 'body' } - requires :password, type:String, desc: 'Password', allow_blank: false, is_equal_to: :password_confirmation, documentation: { param_type: 'body' } - requires :password_confirmation, type:String, desc: 'Password confirmation', allow_blank: false, documentation: { param_type: 'body' } + requires :name, type: String, desc: 'Full name', allow_blank: false, documentation: { param_type: 'body' } + requires :email, type: String, desc: 'Username', allow_blank: false, documentation: { param_type: 'body' } + requires :password, type: String, desc: 'Password', allow_blank: false, is_equal_to: :password_confirmation, documentation: { param_type: 'body' } + requires :password_confirmation, type: String, desc: 'Password confirmation', allow_blank: false, documentation: { param_type: 'body' } end - - end post do declared_params = declared(params) np = nil u = nil Qx.transaction do + np = Nonprofit.new(OnboardAccounts.set_nonprofit_defaults(declared_params[:nonprofit])) + begin - np = Nonprofit.new(OnboardAccounts.set_nonprofit_defaults(declared_params[:nonprofit])) - - begin - np.save! - rescue ActiveRecord::RecordInvalid => e - if (e.record.errors[:slug]) - begin - slug = SlugNonprofitNamingAlgorithm.new(np.state_code_slug, np.city_slug).create_copy_name(np.slug) - np.slug = slug - np.save! - rescue UnableToCreateNameCopyError - raise Grape::Exceptions::ValidationErrors.new(errors:[Grape::Exceptions::Validation.new( - - params: ["nonprofit[name]"], - message: "has an invalid slug. Contact support for help." - )]) - end - else - raise e - end - end - - u = User.new(declared_params[:user]) - u.save! - - role = u.roles.build(host: np, name: 'nonprofit_admin') - role.save! - - billing_plan = BillingPlan.find(Settings.default_bp.id) - b_sub = np.build_billing_subscription(billing_plan: billing_plan, status: 'active') - b_sub.save! + np.save! rescue ActiveRecord::RecordInvalid => e - class_to_name = {Nonprofit => 'nonprofit', User => 'user'} - if class_to_name[e.record.class] - errors = e.record.errors.keys.map {|k| - - errors = e.record.errors[k].uniq - errors.map{|error| Grape::Exceptions::Validation.new( - - params: ["#{class_to_name[e.record.class]}[#{k.to_s}]"], - message: error - - )} - } - - raise Grape::Exceptions::ValidationErrors.new(errors:errors.flatten) + if e.record.errors[:slug] + begin + slug = SlugNonprofitNamingAlgorithm.new(np.state_code_slug, np.city_slug).create_copy_name(np.slug) + np.slug = slug + np.save! + rescue UnableToCreateNameCopyError + raise Grape::Exceptions::ValidationErrors.new(errors: [Grape::Exceptions::Validation.new( + params: ['nonprofit[name]'], + message: 'has an invalid slug. Contact support for help.' + )]) + end else raise e end + end + u = User.new(declared_params[:user]) + u.save! + + role = u.roles.build(host: np, name: 'nonprofit_admin') + role.save! + + billing_plan = BillingPlan.find(Settings.default_bp.id) + b_sub = np.build_billing_subscription(billing_plan: billing_plan, status: 'active') + b_sub.save! + rescue ActiveRecord::RecordInvalid => e + class_to_name = { Nonprofit => 'nonprofit', User => 'user' } + if class_to_name[e.record.class] + errors = e.record.errors.keys.map do |k| + errors = e.record.errors[k].uniq + errors.map do |error| + Grape::Exceptions::Validation.new( + params: ["#{class_to_name[e.record.class]}[#{k}]"], + message: error + ) + end + end + + raise Grape::Exceptions::ValidationErrors.new(errors: errors.flatten) + else + raise e end end - #onboard callback + # onboard callback present np, with: Houdini::V1::Entities::Nonprofit end - - - -end \ No newline at end of file +end diff --git a/app/api/houdini/v1/validations.rb b/app/api/houdini/v1/validations.rb index eb0e60b6..692a5bed 100644 --- a/app/api/houdini/v1/validations.rb +++ b/app/api/houdini/v1/validations.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'houdini/v1/validators/is_equal_to' \ No newline at end of file +require 'houdini/v1/validators/is_equal_to' diff --git a/app/api/houdini/v1/validators/is_equal_to.rb b/app/api/houdini/v1/validators/is_equal_to.rb index c84b8075..73faf52b 100644 --- a/app/api/houdini/v1/validators/is_equal_to.rb +++ b/app/api/houdini/v1/validators/is_equal_to.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Houdini::V1::Validators::IsEqualTo < Grape::Validations::Base def validate_param!(attr_name, params) if params[attr_name] != params[@option] - fail Grape::Exceptions::Validation, params: [@scope.full_name(attr_name), @scope.full_name(@option)], message: message(:is_equal_to) + raise Grape::Exceptions::Validation, params: [@scope.full_name(attr_name), @scope.full_name(@option)], message: message(:is_equal_to) end end -end \ No newline at end of file +end diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb index c2028140..09bc4472 100644 --- a/app/controllers/activities_controller.rb +++ b/app/controllers/activities_controller.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ActivitiesController < ApplicationController + before_action :authenticate_user!, only: [:create] - before_action :authenticate_user!, only: [:create] - - def create - json_saved Activity.create(params[:activity]) - end - + def create + json_saved Activity.create(params[:activity]) + end end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 09be7bba..009393af 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,174 +1,178 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ApplicationController < ActionController::Base - before_action :set_locale, :redirect_to_maintenance + before_action :set_locale, :redirect_to_maintenance - protect_from_forgery + protect_from_forgery - helper_method \ - :current_role?, - :current_nonprofit_user?, - :administered_nonprofit, + helper_method \ + :current_role?, + :current_nonprofit_user?, + :administered_nonprofit, :nonprofit_in_trial?, - :current_plan_tier #int + :current_plan_tier # int - def set_locale - if params[:locale] && Settings.available_locales.include?(params[:locale]) - I18n.locale = params[:locale] - else - I18n.locale = Settings.language - end - end + def set_locale + if params[:locale] && Settings.available_locales.include?(params[:locale]) + I18n.locale = params[:locale] + else + I18n.locale = Settings.language + end + end - def redirect_to_maintenance - if (Settings&.maintenance&.maintenance_mode && !current_user) - unless (self.class == Users::SessionsController && - ((Settings.maintenance.maintenance_token && params[:maintenance_token] == Settings.maintenance.maintenance_token) || params[:format] == 'json')) - redirect_to Settings.maintenance.maintenance_page - end - end - end + def redirect_to_maintenance + if Settings&.maintenance&.maintenance_mode && !current_user + unless self.class == Users::SessionsController && + ((Settings.maintenance.maintenance_token && params[:maintenance_token] == Settings.maintenance.maintenance_token) || params[:format] == 'json') + redirect_to Settings.maintenance.maintenance_page + end + end + end -protected + protected - def json_saved(model, msg=nil) - if model.valid? - flash[:notice] = msg if msg - render json: model, status: 200 - else - render json: model.errors.full_messages, status: :unprocessable_entity - end - end + def json_saved(model, msg = nil) + if model.valid? + flash[:notice] = msg if msg + render json: model, status: 200 + else + render json: model.errors.full_messages, status: :unprocessable_entity + end + end # A response helper for use with the param_validation gem # use like: render_json{ UpdateUsers.update(params[:user]) } # will catch and pretty print exceptions using the rails loggers def render_json(&block) begin - result = {status: 200, json: yield(block)} + result = { status: 200, json: yield(block) } rescue ParamValidation::ValidationError => e logger.info "422: #{e}".red.bold - #logger.info ">>".bold.red + " #{{'Failed key name' => e.data[:key], 'Value' => e.data[:val], 'Failed validator' => e.data[:name]}}".red - result = {status: 422, json: {error: e.message}} - rescue CCOrgError => e - logger.info "422: #{e}".red.bold - result = {status: 422, json: {error: e.message}} + # logger.info ">>".bold.red + " #{{'Failed key name' => e.data[:key], 'Value' => e.data[:val], 'Failed validator' => e.data[:name]}}".red + result = { status: 422, json: { error: e.message } } + rescue CCOrgError => e + logger.info "422: #{e}".red.bold + result = { status: 422, json: { error: e.message } } rescue ActiveRecord::RecordNotFound => e logger.info "404: #{e}".red.bold - result = {status: 404, json: {error: e.message}} - rescue AuthenticationError => e - logger.info "401: #{e}".red.bold - result = {status: 401, json: {error: e.message}} - rescue ExpiredTokenError => e - logger.info "422: #{e}".red.bold - result = {status: 422, json: {error: e.message}} + result = { status: 404, json: { error: e.message } } + rescue AuthenticationError => e + logger.info "401: #{e}".red.bold + result = { status: 401, json: { error: e.message } } + rescue ExpiredTokenError => e + logger.info "422: #{e}".red.bold + result = { status: 422, json: { error: e.message } } rescue Exception => e # a non-validation related exception logger.error "500: #{e}".red.bold - logger.error e.backtrace.take(5).map{|l| ">>".red.bold + " #{l}"}.join("\n").red - result = {status: 500, json: {error: e.message, backtrace: e.backtrace}} + logger.error e.backtrace.take(5).map { |l| '>>'.red.bold + " #{l}" }.join("\n").red + result = { status: 500, json: { error: e.message, backtrace: e.backtrace } } end render result end - # Test that within the last 5 minutes, the user has confirmed their password - def password_was_confirmed(token) - session[:pw_token] == token && Chronic.parse(session[:pw_timestamp]) >= 5.minutes.ago.utc - end + # Test that within the last 5 minutes, the user has confirmed their password + def password_was_confirmed(token) + session[:pw_token] == token && Chronic.parse(session[:pw_timestamp]) >= 5.minutes.ago.utc + end - def store_location - referrer = request.fullpath - no_redirects = ['/users', '/signup', '/signin', '/users/sign_in', '/users/sign_up', '/users/password', '/users/sign_out', /.*\.json.*/, /.*auth\/facebook.*/] - unless request.format.symbol == :json || no_redirects.map{|p| referrer.match(p)}.any? - session[:previous_url] = referrer - end - end + def store_location + referrer = request.fullpath + no_redirects = ['/users', '/signup', '/signin', '/users/sign_in', '/users/sign_up', '/users/password', '/users/sign_out', /.*\.json.*/, %r{.*auth/facebook.*}] + unless request.format.symbol == :json || no_redirects.map { |p| referrer.match(p) }.any? + session[:previous_url] = referrer + end + end - def block_with_sign_in(msg=nil) - store_location + def block_with_sign_in(msg = nil) + store_location if current_user flash[:notice] = "It looks like you're not allowed to access that page. If this seems like a mistake, please contact #{Settings.mailer.email}" redirect_to root_path else msg ||= 'We need to sign you in before you can do that.' - redirect_to new_user_session_path, :flash => {:error => msg} + redirect_to new_user_session_path, flash: { error: msg } end - end + end - def authenticate_user!(options={}) - block_with_sign_in unless current_user - end + def authenticate_user!(_options = {}) + block_with_sign_in unless current_user + end - def authenticate_confirmed_user! - if !current_user - block_with_sign_in - elsif !current_user.confirmed? && !current_role?([:super_associate, :super_admin]) - redirect_to new_user_confirmation_path, flash: {error: 'You need to confirm your account to do that.'} - end - end + def authenticate_confirmed_user! + if !current_user + block_with_sign_in + elsif !current_user.confirmed? && !current_role?(%i[super_associate super_admin]) + redirect_to new_user_confirmation_path, flash: { error: 'You need to confirm your account to do that.' } + end + end - def authenticate_super_associate! - unless current_role?(:super_admin) || current_role?(:super_associate) - block_with_sign_in 'Please login.' - end - end + def authenticate_super_associate! + unless current_role?(:super_admin) || current_role?(:super_associate) + block_with_sign_in 'Please login.' + end + end - def authenticate_super_admin! - unless current_role?(:super_admin) - block_with_sign_in 'Please login.' - end - end + def authenticate_super_admin! + block_with_sign_in 'Please login.' unless current_role?(:super_admin) + end - def current_role?(role_names, host_id = nil) + def current_role?(role_names, host_id = nil) return false unless current_user - role_names = Array(role_names) - key = "current_role_user_#{current_user_id}_names_#{role_names.join("_")}_host_#{host_id}" - QueryRoles.user_has_role?(current_user.id, role_names, host_id) - end - def nonprofit_in_trial?(npo_id=nil) + role_names = Array(role_names) + key = "current_role_user_#{current_user_id}_names_#{role_names.join('_')}_host_#{host_id}" + QueryRoles.user_has_role?(current_user.id, role_names, host_id) + end + + def nonprofit_in_trial?(npo_id = nil) return false if !npo_id && !administered_nonprofit + npo_id ||= administered_nonprofit.id key = "in_trial_user_#{current_user_id}_nonprofit_#{npo_id}" QueryBillingSubscriptions.currently_in_trial?(npo_id) end - def current_plan_tier(npo_id=nil) + def current_plan_tier(npo_id = nil) return 0 if !npo_id && !administered_nonprofit + npo_id ||= administered_nonprofit.id - return 2 if current_role?(:super_admin) - key = "plan_tier_user_#{current_user_id}_nonprofit_#{npo_id}" + return 2 if current_role?(:super_admin) + + key = "plan_tier_user_#{current_user_id}_nonprofit_#{npo_id}" administered_nonprofit ? QueryBillingSubscriptions.plan_tier(npo_id) : 0 - end + end - def administered_nonprofit - return nil unless current_user - key = "administered_nonprofit_user_#{current_user_id}_nonprofit" - Nonprofit.where(id: QueryRoles.host_ids(current_user_id, [:nonprofit_admin, :nonprofit_associate])).last - end + def administered_nonprofit + return nil unless current_user - # devise config + key = "administered_nonprofit_user_#{current_user_id}_nonprofit" + Nonprofit.where(id: QueryRoles.host_ids(current_user_id, %i[nonprofit_admin nonprofit_associate])).last + end - def after_sign_in_path_for(resource) - request.env['omniauth.origin'] || session[:previous_url] || root_path - end + # devise config - def after_sign_up_path_for(resource) - request.env['omniauth.origin'] || session[:previous_url] || root_path - end + def after_sign_in_path_for(_resource) + request.env['omniauth.origin'] || session[:previous_url] || root_path + end - def after_update_path_for(resource) - profile_path(current_user.profile) - end + def after_sign_up_path_for(_resource) + request.env['omniauth.origin'] || session[:previous_url] || root_path + end - def after_inactive_sign_up_path_for(resource) - profile_path(current_user.profile) - end + def after_update_path_for(_resource) + profile_path(current_user.profile) + end - # /devise config + def after_inactive_sign_up_path_for(_resource) + profile_path(current_user.profile) + end -private + # /devise config - def current_user_id - current_user && current_user.id - end + private + def current_user_id + current_user&.id + end end diff --git a/app/controllers/aws_presigned_posts_controller.rb b/app/controllers/aws_presigned_posts_controller.rb index a6855266..b3ed7388 100644 --- a/app/controllers/aws_presigned_posts_controller.rb +++ b/app/controllers/aws_presigned_posts_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AwsPresignedPostsController < ApplicationController before_action :authenticate_user! @@ -7,12 +9,12 @@ class AwsPresignedPostsController < ApplicationController # http://docs.aws.amazon.com/AWSRubySDK/latest/AWS/S3/PresignedPost.html def create uuid = SecureRandom.uuid - p = S3Bucket.presigned_post({ + p = S3Bucket.presigned_post( key: "tmp/#{uuid}/${filename}", success_action_status: 201, acl: 'public-read', expiration: 30.days.from_now - }) + ) render json: { s3_presigned_post: p.fields.to_json, @@ -20,5 +22,4 @@ class AwsPresignedPostsController < ApplicationController s3_uuid: uuid } end - end diff --git a/app/controllers/billing_subscriptions_controller.rb b/app/controllers/billing_subscriptions_controller.rb index cee634ed..258d3756 100644 --- a/app/controllers/billing_subscriptions_controller.rb +++ b/app/controllers/billing_subscriptions_controller.rb @@ -1,30 +1,32 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingSubscriptionsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! def create_trial - render JsonResp.new(params){|params| + render JsonResp.new(params) do |_params| requires(:nonprofit_id).as_int requires(:stripe_plan_id).as_string - }.when_valid{|params| + end.when_valid do |params| InsertBillingSubscriptions.trial(params[:nonprofit_id], params[:stripe_plan_id]) - } + end end - def create + def create @nonprofit ||= Nonprofit.find(params[:nonprofit_id]) - @subscription = BillingSubscription.create_with_stripe(@nonprofit, params[:billing_subscription]) - json_saved(@subscription, "Success! You are subscribed to #{Settings.general.name}.") - end + @subscription = BillingSubscription.create_with_stripe(@nonprofit, params[:billing_subscription]) + json_saved(@subscription, "Success! You are subscribed to #{Settings.general.name}.") + end # post /nonprofits/:nonprofit_id/billing_subscription/cancel - def cancel - @result = CancelBillingSubscription.with_stripe(@nonprofit) - flash[:notice] = "Your subscription has been cancelled. We'll email you soon with exports." + def cancel + @result = CancelBillingSubscription.with_stripe(@nonprofit) + flash[:notice] = "Your subscription has been cancelled. We'll email you soon with exports." redirect_to root_url - end + end # get nonprofits/:nonprofit_id/billing_subscription/cancellation def cancellation diff --git a/app/controllers/button_debug_controller.rb b/app/controllers/button_debug_controller.rb index 9194b0a8..53d8a892 100644 --- a/app/controllers/button_debug_controller.rb +++ b/app/controllers/button_debug_controller.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ButtonDebugController < ApplicationController def embedded @np = params[:id] || 1 - respond_to { |format| format.html{render layout: 'layouts/empty'} } + respond_to { |format| format.html { render layout: 'layouts/empty' } } end def button @np = params[:id] || 1 - respond_to { |format| format.html{render layout: 'layouts/empty'} } + respond_to { |format| format.html { render layout: 'layouts/empty' } } end end diff --git a/app/controllers/campaign_gift_options_controller.rb b/app/controllers/campaign_gift_options_controller.rb index 044aab0b..83a19633 100644 --- a/app/controllers/campaign_gift_options_controller.rb +++ b/app/controllers/campaign_gift_options_controller.rb @@ -1,29 +1,31 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGiftOptionsController < ApplicationController - include Controllers::CampaignHelper + include Controllers::CampaignHelper - before_action :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order] + before_action :authenticate_campaign_editor!, only: %i[create destroy update update_order] - def index - @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') - render json: {data: @gift_options} - end + def index + @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') + render json: { data: @gift_options } + end - def show - render json: {data: current_campaign.campaign_gift_options.find(params[:id])} - end + def show + render json: { data: current_campaign.campaign_gift_options.find(params[:id]) } + end - def create - campaign = current_campaign - json_saved CreateCampaignGiftOption.create(campaign, params[:campaign_gift_option]), - 'Gift option successfully created!' - end + def create + campaign = current_campaign + json_saved CreateCampaignGiftOption.create(campaign, params[:campaign_gift_option]), + 'Gift option successfully created!' + end - def update - @campaign = current_campaign - gift_option = @campaign.campaign_gift_options.find params[:id] - json_saved UpdateCampaignGiftOption.update(gift_option, params[:campaign_gift_option]), 'Successfully updated' - end + def update + @campaign = current_campaign + gift_option = @campaign.campaign_gift_options.find params[:id] + json_saved UpdateCampaignGiftOption.update(gift_option, params[:campaign_gift_option]), 'Successfully updated' + end # put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order # Pass in {data: [{id: 1, order: 1}]} @@ -32,9 +34,9 @@ class CampaignGiftOptionsController < ApplicationController render json: updated_gift_options end - def destroy - @campaign = current_campaign + def destroy + @campaign = current_campaign - render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id])} - end + render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id]) } + end end diff --git a/app/controllers/campaign_gifts_controller.rb b/app/controllers/campaign_gifts_controller.rb index 786173ff..30006e25 100644 --- a/app/controllers/campaign_gifts_controller.rb +++ b/app/controllers/campaign_gifts_controller.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGiftsController < ApplicationController - # post /campaign_gifts - def create - json_saved CreateCampaignGift.create params[:campaign_gift] - end + def create + json_saved CreateCampaignGift.create params[:campaign_gift] + end end diff --git a/app/controllers/campaigns/campaign_gift_options_controller.rb b/app/controllers/campaigns/campaign_gift_options_controller.rb index eba8451d..c9339d83 100644 --- a/app/controllers/campaigns/campaign_gift_options_controller.rb +++ b/app/controllers/campaigns/campaign_gift_options_controller.rb @@ -1,51 +1,51 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Campaigns; class CampaignGiftOptionsController < ApplicationController - include Controllers::CampaignHelper +module Campaigns + class CampaignGiftOptionsController < ApplicationController + include Controllers::CampaignHelper - before_action :authenticate_campaign_editor!, only: [:create, :destroy, :update, :update_order, :report] + before_action :authenticate_campaign_editor!, only: %i[create destroy update update_order report] - def report - respond_to do |format| - format.json do - render json: QueryCampaignGifts.report_metrics(current_campaign.id) - end - end - end + def report + respond_to do |format| + format.json do + render json: QueryCampaignGifts.report_metrics(current_campaign.id) + end + end + end + def index + @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') + render json: { data: @gift_options } + end + def show + render json: { data: current_campaign.campaign_gift_options.find(params[:id]) } + end - def index - @gift_options = current_campaign.campaign_gift_options.order('"order", amount_recurring, amount_one_time') - render json: {data: @gift_options} - end + def create + campaign = current_campaign + json_saved CreateCampaignGiftOption.create(campaign, params[:campaign_gift_option]), + 'Gift option successfully created!' + end - def show - render json: {data: current_campaign.campaign_gift_options.find(params[:id])} - end + def update + @campaign = current_campaign + gift_option = @campaign.campaign_gift_options.find params[:id] + json_saved UpdateCampaignGiftOption.update(gift_option, params[:campaign_gift_option]), 'Successfully updated' + end - def create - campaign = current_campaign - json_saved CreateCampaignGiftOption.create(campaign, params[:campaign_gift_option]), - 'Gift option successfully created!' - end + # put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order + # Pass in {data: [{id: 1, order: 1}]} + def update_order + updated_gift_options = UpdateOrder.with_data('campaign_gift_options', params[:data]) + render json: updated_gift_options + end - def update - @campaign = current_campaign - gift_option = @campaign.campaign_gift_options.find params[:id] - json_saved UpdateCampaignGiftOption.update(gift_option, params[:campaign_gift_option]), 'Successfully updated' - end - - # put /nonprofits/:nonprofit_id/campaigns/:campaign_id/campaign_gift_options/update_order - # Pass in {data: [{id: 1, order: 1}]} - def update_order - updated_gift_options = UpdateOrder.with_data('campaign_gift_options', params[:data]) - render json: updated_gift_options - end - - def destroy - @campaign = current_campaign - - render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id])} - end + def destroy + @campaign = current_campaign + render_json { DeleteCampaignGiftOption.delete(@campaign, params[:id]) } + end end; end diff --git a/app/controllers/campaigns/donations_controller.rb b/app/controllers/campaigns/donations_controller.rb index a53c0f3d..9cb40393 100644 --- a/app/controllers/campaigns/donations_controller.rb +++ b/app/controllers/campaigns/donations_controller.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Campaigns -class DonationsController < ApplicationController - include Controllers::CampaignHelper + class DonationsController < ApplicationController + include Controllers::CampaignHelper - before_action :authenticate_campaign_editor!, only: [:index] - - def index - respond_to do |format| - format.csv do - file_date = Date.today.strftime("%m-%d-%Y") - donations = QueryDonations.campaign_export(current_campaign.id) - send_data(Format::Csv.from_vectors(donations), filename: "campaign-donations-#{file_date}.csv") - end - end - end + before_action :authenticate_campaign_editor!, only: [:index] -end + def index + respond_to do |format| + format.csv do + file_date = Date.today.strftime('%m-%d-%Y') + donations = QueryDonations.campaign_export(current_campaign.id) + send_data(Format::Csv.from_vectors(donations), filename: "campaign-donations-#{file_date}.csv") + end + end + end + end end diff --git a/app/controllers/campaigns/supporters_controller.rb b/app/controllers/campaigns/supporters_controller.rb index 5f77ffd0..a52fb608 100644 --- a/app/controllers/campaigns/supporters_controller.rb +++ b/app/controllers/campaigns/supporters_controller.rb @@ -1,22 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Campaigns -class SupportersController < ApplicationController - include Controllers::CampaignHelper + class SupportersController < ApplicationController + include Controllers::CampaignHelper - before_action :authenticate_campaign_editor!, only: [:index] + before_action :authenticate_campaign_editor!, only: [:index] - def index - @panels_layout = true - @nonprofit = current_nonprofit - @campaign = current_campaign - - respond_to do |format| - format.json do - render json: QuerySupporters.campaign_list(@nonprofit.id, @campaign.id, params) - end - format.html - end - end + def index + @panels_layout = true + @nonprofit = current_nonprofit + @campaign = current_campaign -end + respond_to do |format| + format.json do + render json: QuerySupporters.campaign_list(@nonprofit.id, @campaign.id, params) + end + format.html + end + end + end end diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index a6d34d21..9868de95 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignsController < ApplicationController include Controllers::CampaignHelper helper_method :current_campaign_editor? - before_action :authenticate_confirmed_user!, only: [:create, :name_and_id, :duplicate] - before_action :authenticate_campaign_editor!, only: [:update, :soft_delete] - before_action :check_nonprofit_status, only: [:index, :show] + before_action :authenticate_confirmed_user!, only: %i[create name_and_id duplicate] + before_action :authenticate_campaign_editor!, only: %i[update soft_delete] + before_action :check_nonprofit_status, only: %i[index show] def index @nonprofit = current_nonprofit - if (current_nonprofit_user?) + if current_nonprofit_user? @campaigns = @nonprofit.campaigns.includes(:nonprofit).not_deleted.order('created_at desc') @deleted_campaigns = @nonprofit.campaigns.includes(:nonprofit).deleted.order('created_at desc') else @@ -70,14 +72,11 @@ class CampaignsController < ApplicationController # post 'nonprofits/:np_id/campaigns/:campaign_id/duplicate' def duplicate - - render_json { + render_json do InsertDuplicate.campaign(current_campaign.id, current_user.profile.id) - } - + end end - def soft_delete current_campaign.update_attribute(:deleted, params[:delete]) render json: {} @@ -101,17 +100,17 @@ class CampaignsController < ApplicationController end def peer_to_peer - session[:donor_signup_url] = request.env["REQUEST_URI"] + session[:donor_signup_url] = request.env['REQUEST_URI'] @nonprofit = Nonprofit.find_by_id(params[:npo_id]) @parent_campaign = Campaign.find_by_id(params[:campaign_id]) if params[:campaign_id].present? && !@parent_campaign - raise ActionController::RoutingError.new('Not Found') + raise ActionController::RoutingError, 'Not Found' end if current_user @profile = current_user.profile - if (@parent_campaign) + if @parent_campaign @child_campaign = Campaign.where( profile_id: @profile.id, parent_campaign_id: @parent_campaign.id @@ -124,7 +123,7 @@ class CampaignsController < ApplicationController def check_nonprofit_status if !current_role?(:super_admin) && !current_nonprofit.published - raise ActionController::RoutingError.new('Not Found') + raise ActionController::RoutingError, 'Not Found' end end end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index bcd7c72b..2d13af20 100755 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CardsController < ApplicationController + before_action :authenticate_user!, except: [:create] - before_action :authenticate_user!, :except => [:create] - - # post /cards - def create + # post /cards + def create acct = Supporter.find(params[:card][:holder_id]).nonprofit.stripe_account_id render( - JsonResp.new(params) do |d| + JsonResp.new(params) do |_d| requires(:card).nested do requires(:name, :stripe_card_token).as_string requires(:holder_id).as_int requires(:holder_type).one_of('Supporter') end end.when_valid do |d| - InsertCard.with_stripe(d[:card], acct, params[:event_id], current_user) + InsertCard.with_stripe(d[:card], acct, params[:event_id], current_user) end ) - end - + end end diff --git a/app/controllers/direct_debit_details_controller.rb b/app/controllers/direct_debit_details_controller.rb index ea9e00bb..52c7426d 100644 --- a/app/controllers/direct_debit_details_controller.rb +++ b/app/controllers/direct_debit_details_controller.rb @@ -1,19 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class DirectDebitDetailsController < ApplicationController - - # POST /sepa # This endpoint is used for saving direct debit account details # when SEPA payment is selected in the donation widget. Actual charge is # happening offline, after donations are exported to an external CRM. def create render( - JsonResp.new(params) do |data| + JsonResp.new(params) do |_data| requires(:supporter_id).as_int requires(:sepa_params).nested do requires(:iban, :name, :bic).as_string end - end.when_valid do |data| + end.when_valid do |_data| InsertDirectDebitDetail.execute(params) end ) diff --git a/app/controllers/email_settings_controller.rb b/app/controllers/email_settings_controller.rb index 1c94c8b2..c376e30c 100644 --- a/app/controllers/email_settings_controller.rb +++ b/app/controllers/email_settings_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailSettingsController < ApplicationController include Controllers::NonprofitHelper @@ -15,6 +17,4 @@ class EmailSettingsController < ApplicationController user = current_role?(:super_admin) ? User.find(params[:user_id]) : current_user render json: UpdateEmailSettings.save(params[:nonprofit_id], user.id, params[:email_settings]) end - end - diff --git a/app/controllers/emails_controller.rb b/app/controllers/emails_controller.rb index 60c2b324..95e19c7f 100644 --- a/app/controllers/emails_controller.rb +++ b/app/controllers/emails_controller.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailsController < ApplicationController - before_action :authenticate_user! - - def create - email = params[:email] - GenericMailer.delay.generic_mail(email[:from_email], email[:from_name], email[:message], email[:subject], email[:to_email], email[:to_name]) - render :json => {:notification => 'Email successfully sent'}, :status => :created - end + before_action :authenticate_user! + def create + email = params[:email] + GenericMailer.delay.generic_mail(email[:from_email], email[:from_name], email[:message], email[:subject], email[:to_email], email[:to_name]) + render json: { notification: 'Email successfully sent' }, status: :created + end end diff --git a/app/controllers/event_discounts_controller.rb b/app/controllers/event_discounts_controller.rb index 167637c2..c696d872 100644 --- a/app/controllers/event_discounts_controller.rb +++ b/app/controllers/event_discounts_controller.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventDiscountsController < ApplicationController include Controllers::EventHelper - before_action :authenticate_event_editor!, :except => [:index] + before_action :authenticate_event_editor!, except: [:index] def create params[:event_discount][:event_id] = current_event.id - render JsonResp.new(params[:event_discount]){|data| + render JsonResp.new(params[:event_discount]) do |_data| requires(:code, :name).as_string requires(:event_id, :percent).as_int - }.when_valid{|data| + end.when_valid do |data| { status: 200, json: { event_discount: current_event.event_discounts.create(data) } } - } + end end def index @@ -26,15 +28,14 @@ class EventDiscountsController < ApplicationController .returning('*') ).first ) - render json: {status: 200, data: discount } + render json: { status: 200, data: discount } end def destroy Psql.execute( - Qexpr.new.delete_from("event_discounts") - .where("event_discounts.event_id=$id", id: params["event_id"]) - .where("event_discounts.id=$id", id: params["id"]) + Qexpr.new.delete_from('event_discounts') + .where('event_discounts.event_id=$id', id: params['event_id']) + .where('event_discounts.id=$id', id: params['id']) ) end - end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index b0c82467..2817c9c7 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -1,21 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventsController < ApplicationController - include Controllers::EventHelper + include Controllers::EventHelper - helper_method :current_event_editor? + helper_method :current_event_editor? before_action :authenticate_nonprofit_user!, only: :name_and_id - before_action :authenticate_event_editor!, only: [:update, :soft_delete, :stats, :create, :duplicate] + before_action :authenticate_event_editor!, only: %i[update soft_delete stats create duplicate] - - def index + def index @nonprofit = current_nonprofit - end + end def listings render json: QueryEventMetrics.for_listings('nonprofit', current_nonprofit.id, params) end - def show + def show @event = params[:event_slug] ? Event.find_by_slug!(params[:event_slug]) : Event.find_by_id!(params[:id]) @event_background_image = FetchBackgroundImage.with_model(@event) @nonprofit = @event.nonprofit @@ -24,10 +25,10 @@ class EventsController < ApplicationController flash[:notice] = "Sorry, we couldn't find that event" return end - @organizer = QueryEventOrganizer.with_event(@event.id) - end + @organizer = QueryEventOrganizer.with_event(@event.id) + end - def create + def create render_json do Time.use_zone(current_nonprofit.timezone || 'UTC') do params[:event][:start_datetime] = Chronic.parse(params[:event][:start_datetime]) if params[:event][:start_datetime].present? @@ -35,22 +36,22 @@ class EventsController < ApplicationController end flash[:notice] = 'Your draft event has been created! Well done.' ev = current_nonprofit.events.create(params[:event]) - {url: "/events/#{ev.slug}", event: ev} + { url: "/events/#{ev.slug}", event: ev } end - end + end - def update + def update Time.use_zone(current_nonprofit.timezone || 'UTC') do params[:event][:start_datetime] = Chronic.parse(params[:event][:start_datetime]) if params[:event][:start_datetime].present? params[:event][:end_datetime] = Chronic.parse(params[:event][:end_datetime]) if params[:event][:end_datetime].present? end - current_event.update_attributes(params[:event]) - json_saved current_event, 'Successfully updated' - end + current_event.update_attributes(params[:event]) + json_saved current_event, 'Successfully updated' + end # post 'nonprofits/:np_id/events/:event_id/duplicate' def duplicate - render_json { InsertDuplicate.event(current_event.id, current_user.profile.id)} + render_json { InsertDuplicate.event(current_event.id, current_user.profile.id) } end def activities @@ -58,24 +59,22 @@ class EventsController < ApplicationController end def soft_delete - current_event.update_attribute(:deleted, params[:delete]) - render json: {} - end + current_event.update_attribute(:deleted, params[:delete]) + render json: {} + end - def metrics + def metrics render json: QueryEventMetrics.with_event_ids([current_event.id]).first - end + end - def stats - @event = current_event - @url = Format::Url.concat(root_url, @event.url) - @event_background_image = FetchBackgroundImage.with_model(@event) - render layout: 'layouts/embed' - end + def stats + @event = current_event + @url = Format::Url.concat(root_url, @event.url) + @event_background_image = FetchBackgroundImage.with_model(@event) + render layout: 'layouts/embed' + end def name_and_id render json: QueryEvents.name_and_id(current_nonprofit.id) end - - end diff --git a/app/controllers/front_controller.rb b/app/controllers/front_controller.rb index e43cf7b7..da3b077f 100755 --- a/app/controllers/front_controller.rb +++ b/app/controllers/front_controller.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FrontController < ApplicationController def index - if !Nonprofit.any? - redirect_to onboard_path - elsif current_role?([:nonprofit_admin,:nonprofit_associate]) - redirect_to NonprofitPath.dashboard(administered_nonprofit) - elsif current_user - redirect_to '/profiles/' + current_user.profile.id.to_s - else - redirect_to new_user_session_path - end - end + if Nonprofit.none? + redirect_to onboard_path + elsif current_role?(%i[nonprofit_admin nonprofit_associate]) + redirect_to NonprofitPath.dashboard(administered_nonprofit) + elsif current_user + redirect_to '/profiles/' + current_user.profile.id.to_s + else + redirect_to new_user_session_path + end + end end diff --git a/app/controllers/image_attachments_controller.rb b/app/controllers/image_attachments_controller.rb index 9f47cbbd..fc1d7b13 100644 --- a/app/controllers/image_attachments_controller.rb +++ b/app/controllers/image_attachments_controller.rb @@ -1,24 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ImageAttachmentsController < ApplicationController - before_action :authenticate_confirmed_user! - def create - # must return json with a link attr - # http://editor.froala.com/server-integrations/php-image-upload - @image = ImageAttachment.new(:file => params[:file]) - if @image.save - render :json => {:link => @image.file_url} - else - render :json => @image.errors.full_messages, :status => :unprocessable_entity - end - end + before_action :authenticate_confirmed_user! + def create + # must return json with a link attr + # http://editor.froala.com/server-integrations/php-image-upload + @image = ImageAttachment.new(file: params[:file]) + if @image.save + render json: { link: @image.file_url } + else + render json: @image.errors.full_messages, status: :unprocessable_entity + end + end - def remove - @image = ImageAttachment.select{|img| img.file_url == params[:src]}.first - if @image - @image.destroy - render :json => @image - else - render :json => {}, :status => :unprocessable_entity - end - end + def remove + @image = ImageAttachment.select { |img| img.file_url == params[:src] }.first + if @image + @image.destroy + render json: @image + else + render json: {}, status: :unprocessable_entity + end + end end diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 1e5df927..2982879a 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -1,34 +1,34 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class MapsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper - before_action :authenticate_super_associate!, only: :all_supporters - before_action :authenticate_nonprofit_user!, only: [:all_npo_supporters, :specific_npo_supporters] + before_action :authenticate_super_associate!, only: :all_supporters + before_action :authenticate_nonprofit_user!, only: %i[all_npo_supporters specific_npo_supporters] - # used on admin/nonprofits_map and front page - def all_npos - respond_to do |format| - format.html { redirect_to :root } - format.json { @map_data = Nonprofit.where("latitude IS NOT NULL").last(1000) } - end - end + # used on admin/nonprofits_map and front page + def all_npos + respond_to do |format| + format.html { redirect_to :root } + format.json { @map_data = Nonprofit.where('latitude IS NOT NULL').last(1000) } + end + end - # used on admin/supporters_map - def all_supporters - @map_data = Supporter.where("latitude IS NOT NULL").last(1000) - end - - # used on npo dashboard - def all_npo_supporters - @map_data = Nonprofit.find(params['npo_id']).supporters.where("latitude IS NOT NULL").last(100) - end - - # used on supporter dashboard - def specific_npo_supporters - supporter_ids = params['supporter_ids'].split(",").map { |s| s.to_i } - supporters = Nonprofit.find(params['npo_id']).supporters.find(supporter_ids).last(500) - @map_data = supporters.map{|s| s if s.latitude != ''} - end + # used on admin/supporters_map + def all_supporters + @map_data = Supporter.where('latitude IS NOT NULL').last(1000) + end + # used on npo dashboard + def all_npo_supporters + @map_data = Nonprofit.find(params['npo_id']).supporters.where('latitude IS NOT NULL').last(100) + end + # used on supporter dashboard + def specific_npo_supporters + supporter_ids = params['supporter_ids'].split(',').map(&:to_i) + supporters = Nonprofit.find(params['npo_id']).supporters.find(supporter_ids).last(500) + @map_data = supporters.map { |s| s if s.latitude != '' } + end end diff --git a/app/controllers/nonprofits/activities_controller.rb b/app/controllers/nonprofits/activities_controller.rb index 87696c3a..d5d6b652 100644 --- a/app/controllers/nonprofits/activities_controller.rb +++ b/app/controllers/nonprofits/activities_controller.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class ActivitiesController < ApplicationController - include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + class ActivitiesController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_user! # get /nonprofits/:nonprofit_id/supporters/:supporter_id/activities def index render json: QueryActivities.for_timeline(params[:nonprofit_id], params[:supporter_id]) end - - end + end end - diff --git a/app/controllers/nonprofits/bank_accounts_controller.rb b/app/controllers/nonprofits/bank_accounts_controller.rb index 9073c33d..43dfbc31 100644 --- a/app/controllers/nonprofits/bank_accounts_controller.rb +++ b/app/controllers/nonprofits/bank_accounts_controller.rb @@ -1,64 +1,65 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class BankAccountsController < ApplicationController - include Controllers::NonprofitHelper + class BankAccountsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! - # post /nonprofits/:nonprofit_id/bank_account - # must pass in the user's password as params[:password] - def create - if password_was_confirmed(params[:pw_token]) - render_json { InsertBankAccount.with_stripe(current_nonprofit, current_user, params[:bank_account]) } - else - render json: ["Please confirm your password"], status: :unprocessable_entity - end - end + # post /nonprofits/:nonprofit_id/bank_account + # must pass in the user's password as params[:password] + def create + if password_was_confirmed(params[:pw_token]) + render_json { InsertBankAccount.with_stripe(current_nonprofit, current_user, params[:bank_account]) } + else + render json: ['Please confirm your password'], status: :unprocessable_entity + end + end - # get /nonprofits/:nonprofit_id/bank_account/confirmation - def confirmation - @nonprofit = Nonprofit.find(params[:nonprofit_id]) - @bank_account = @nonprofit.bank_account - end + # get /nonprofits/:nonprofit_id/bank_account/confirmation + def confirmation + @nonprofit = Nonprofit.find(params[:nonprofit_id]) + @bank_account = @nonprofit.bank_account + end - # post /nonprofits/:nonprofit_id/bank_account/confirmation - def confirm - npo = current_nonprofit - ba = npo.bank_account - if params[:token] == ba.confirmation_token - ba.update_attribute(:pending_verification, false) - flash[:notice] = "Your bank account is now confirmed!" - redirect_to nonprofits_payouts_path(npo) - else - redirect_to(nonprofits_donations_path(npo), {:flash => {:error => "We could not confirm this bank account. Please follow the exact link provided in the confirmation email."}}) - end - end + # post /nonprofits/:nonprofit_id/bank_account/confirmation + def confirm + npo = current_nonprofit + ba = npo.bank_account + if params[:token] == ba.confirmation_token + ba.update_attribute(:pending_verification, false) + flash[:notice] = 'Your bank account is now confirmed!' + redirect_to nonprofits_payouts_path(npo) + else + redirect_to(nonprofits_donations_path(npo), flash: { error: 'We could not confirm this bank account. Please follow the exact link provided in the confirmation email.' }) + end + end - # get /nonprofits/:nonprofit_id/bank_account/cancellation - def cancellation - @nonprofit = Nonprofit.find(params[:nonprofit_id]) - @bank_account = @nonprofit.bank_account - end + # get /nonprofits/:nonprofit_id/bank_account/cancellation + def cancellation + @nonprofit = Nonprofit.find(params[:nonprofit_id]) + @bank_account = @nonprofit.bank_account + end - # post /nonprofits/:nonprofit_id/bank_account/cancel - def cancel - npo = current_nonprofit - ba = npo.bank_account - if params[:token] == ba.confirmation_token - ba.destroy - flash[:notice] = "Your bank account has been removed." - redirect_to nonprofits_donations_path(npo) - else - redirect_to(nonprofits_donations_path(npo), {:flash => {:error => "We could not remove this bank account. Please follow the exact link provided in the email."}}) - end - end - - def resend_confirmation - npo = current_nonprofit - ba = npo.bank_account - NonprofitMailer.delay.new_bank_account_notification(ba) if ba.valid? - respond_to{|format| format.json{render json: {}}} - end + # post /nonprofits/:nonprofit_id/bank_account/cancel + def cancel + npo = current_nonprofit + ba = npo.bank_account + if params[:token] == ba.confirmation_token + ba.destroy + flash[:notice] = 'Your bank account has been removed.' + redirect_to nonprofits_donations_path(npo) + else + redirect_to(nonprofits_donations_path(npo), flash: { error: 'We could not remove this bank account. Please follow the exact link provided in the email.' }) + end + end -end + def resend_confirmation + npo = current_nonprofit + ba = npo.bank_account + NonprofitMailer.delay.new_bank_account_notification(ba) if ba.valid? + respond_to { |format| format.json { render json: {} } } + end + end end diff --git a/app/controllers/nonprofits/button_controller.rb b/app/controllers/nonprofits/button_controller.rb index a2258b36..35fb85a6 100644 --- a/app/controllers/nonprofits/button_controller.rb +++ b/app/controllers/nonprofits/button_controller.rb @@ -1,28 +1,27 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class ButtonController < ApplicationController - include Controllers::NonprofitHelper + class ButtonController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_user! - before_action :authenticate_user! + def send_code + NonprofitMailer.button_code(current_nonprofit, params[:to_email], params[:to_name], params[:from_email], params[:message], params[:code]).deliver + render json: {}, status: 200 + end + def basic + @nonprofit = current_nonprofit + end - def send_code - NonprofitMailer.button_code(current_nonprofit, params[:to_email], params[:to_name], params[:from_email], params[:message], params[:code]).deliver - render json: {}, status: 200 - end + def guided + @nonprofit = current_nonprofit + end - def basic - @nonprofit = current_nonprofit - end - - def guided - @nonprofit = current_nonprofit - end - - def advanced - @nonprofit = current_nonprofit - end - -end + def advanced + @nonprofit = current_nonprofit + end + end end diff --git a/app/controllers/nonprofits/cards_controller.rb b/app/controllers/nonprofits/cards_controller.rb index 1ee6fdee..8a8f0446 100644 --- a/app/controllers/nonprofits/cards_controller.rb +++ b/app/controllers/nonprofits/cards_controller.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class CardsController < ApplicationController + class CardsController < ApplicationController include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! def edit @nonprofit = current_nonprofit @@ -12,7 +14,7 @@ module Nonprofits # POST /nonprofits/:nonprofit_id/card def create render( - JsonResp.new(params) do |d| + JsonResp.new(params) do |_d| requires(:nonprofit_id).as_int requires(:card).nested do requires(:name, :stripe_card_token, :stripe_card_id).as_string @@ -25,6 +27,5 @@ module Nonprofits end ) end - end end diff --git a/app/controllers/nonprofits/charges_controller.rb b/app/controllers/nonprofits/charges_controller.rb index 25ee872d..4825b881 100644 --- a/app/controllers/nonprofits/charges_controller.rb +++ b/app/controllers/nonprofits/charges_controller.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class ChargesController < ApplicationController - include Controllers::NonprofitHelper + class ChargesController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user!, only: :index + before_action :authenticate_nonprofit_user!, only: :index - # get /nonprofit/:nonprofit_id/charges - def index - redirect_to controller: :payments, action: :index - end # def index - - end + # get /nonprofit/:nonprofit_id/charges + def index + redirect_to controller: :payments, action: :index + end # def index + end end diff --git a/app/controllers/nonprofits/custom_field_joins_controller.rb b/app/controllers/nonprofits/custom_field_joins_controller.rb index 2c36cfaa..1cf26ea8 100644 --- a/app/controllers/nonprofits/custom_field_joins_controller.rb +++ b/app/controllers/nonprofits/custom_field_joins_controller.rb @@ -1,41 +1,39 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class CustomFieldJoinsController < ApplicationController + class CustomFieldJoinsController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_user! - include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + def index + @custom_field_joins = current_nonprofit + .supporters.find(params[:supporter_id]) + .custom_field_joins + .order('created_at DESC') + end - def index - @custom_field_joins = current_nonprofit - .supporters.find(params[:supporter_id]) - .custom_field_joins - .order('created_at DESC') - end + # used for modify a single supporter's custom fields or a group of + # selected supporters' CFs or all supporters' CFs + def modify + if params[:custom_fields].blank? || params[:custom_fields].empty? + render json: {} + return + end - # used for modify a single supporter's custom fields or a group of - # selected supporters' CFs or all supporters' CFs - def modify - if params[:custom_fields].blank? || params[:custom_fields].empty? - render json: {} - return - end - - if params[:selecting_all] - supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select("supporters.id").execute.map{|h| h['id']} - else - supporter_ids = params[:supporter_ids]. map(&:to_i) - end + if params[:selecting_all] + supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select('supporters.id').execute.map { |h| h['id'] } + else + supporter_ids = params[:supporter_ids]. map(&:to_i) + end - render InsertCustomFieldJoins.in_bulk(current_nonprofit.id, supporter_ids, params[:custom_fields]) - end + render InsertCustomFieldJoins.in_bulk(current_nonprofit.id, supporter_ids, params[:custom_fields]) + end - - def destroy - supporter = current_nonprofit.supporters.find(params[:supporter_id]) - supporter.custom_field_joins.find(params[:id]).destroy - render json: {}, status: :ok - end - - end + def destroy + supporter = current_nonprofit.supporters.find(params[:supporter_id]) + supporter.custom_field_joins.find(params[:id]).destroy + render json: {}, status: :ok + end + end end - diff --git a/app/controllers/nonprofits/custom_field_masters_controller.rb b/app/controllers/nonprofits/custom_field_masters_controller.rb index 17fad57b..eda2ab8a 100644 --- a/app/controllers/nonprofits/custom_field_masters_controller.rb +++ b/app/controllers/nonprofits/custom_field_masters_controller.rb @@ -1,27 +1,27 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class CustomFieldMastersController < ApplicationController - include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + class CustomFieldMastersController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_user! - def index - @custom_field_masters = current_nonprofit - .custom_field_masters - .order('id DESC') - .not_deleted - end + def index + @custom_field_masters = current_nonprofit + .custom_field_masters + .order('id DESC') + .not_deleted + end - def create - json_saved CreateCustomFieldMaster.create(current_nonprofit, params[:custom_field_master]) - end + def create + json_saved CreateCustomFieldMaster.create(current_nonprofit, params[:custom_field_master]) + end - def destroy - custom_field_master = current_nonprofit.custom_field_masters.find(params[:id]) - custom_field_master.update_attribute(:deleted, true) - custom_field_master.custom_field_joins.destroy_all - render json: {}, status: :ok - end - - end + def destroy + custom_field_master = current_nonprofit.custom_field_masters.find(params[:id]) + custom_field_master.update_attribute(:deleted, true) + custom_field_master.custom_field_joins.destroy_all + render json: {}, status: :ok + end + end end - diff --git a/app/controllers/nonprofits/donations_controller.rb b/app/controllers/nonprofits/donations_controller.rb index 80cd72f0..99b9cf27 100644 --- a/app/controllers/nonprofits/donations_controller.rb +++ b/app/controllers/nonprofits/donations_controller.rb @@ -1,84 +1,83 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class DonationsController < ApplicationController - include Controllers::NonprofitHelper + class DonationsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user!, only: [:index, :update] - before_action :authenticate_campaign_editor!, only: [:create_offsite] + before_action :authenticate_nonprofit_user!, only: %i[index update] + before_action :authenticate_campaign_editor!, only: [:create_offsite] - # get /nonprofit/:nonprofit_id/donations - def index - redirect_to controller: :payments, action: :index - end # def index + # get /nonprofit/:nonprofit_id/donations + def index + redirect_to controller: :payments, action: :index + end # def index - # post /nonprofits/:nonprofit_id/donations - def create + # post /nonprofits/:nonprofit_id/donations + def create + if params[:token] + params[:donation][:token] = params[:token] + render_json { InsertDonation.with_stripe(params[:donation], current_user) } + elsif params[:direct_debit_detail_id] + render JsonResp.new(params[:donation]) do |_data| + requires(:amount).as_int + requires(:supporter_id, :nonprofit_id) + # TODO + # requires_either(:card_id, :direct_debit_detail_id).as_int + optional(:dedication, :designation).as_string + optional(:campaign_id, :event_id).as_int + end.when_valid do |data| - if params[:token] - params[:donation][:token] = params[:token] - return render_json{ InsertDonation.with_stripe(params[:donation], current_user) } - elsif params[:direct_debit_detail_id] - render JsonResp.new(params[:donation]){|data| - requires(:amount).as_int - requires(:supporter_id, :nonprofit_id) - # TODO - # requires_either(:card_id, :direct_debit_detail_id).as_int - optional(:dedication, :designation).as_string - optional(:campaign_id, :event_id).as_int - }.when_valid{|data| + InsertDonation.with_sepa(data) + end + end + end - - InsertDonation.with_sepa(data) - - } - end - end - - # post /nonprofits/:nonprofit_id/donations/create_offsite - def create_offsite - render JsonResp.new(params[:donation]){|data| + # post /nonprofits/:nonprofit_id/donations/create_offsite + def create_offsite + render JsonResp.new(params[:donation]) do |_data| requires(:amount).as_int.min(1) requires(:supporter_id, :nonprofit_id).as_int optional(:dedication, :designation).as_string optional(:campaign_id, :event_id).as_int optional(:date).as_date - optional(:offsite_payment).nested{ + optional(:offsite_payment).nested do optional(:kind).one_of('cash', 'check') optional(:check_number) - } - }.when_valid{|data| InsertDonation.offsite(data)} - end + end + end.when_valid { |data| InsertDonation.offsite(data) } + end - def update - render_json{ UpdateDonation.update_payment(params[:id], params[:donation]) } - end + def update + render_json { UpdateDonation.update_payment(params[:id], params[:donation]) } + end - # put /nonprofits/:nonprofit_id/donations/:id - # update designation, dedication, or comment on a donation in the followup - def followup - nonprofit = Nonprofit.find(params[:nonprofit_id]) - donation = nonprofit.donations.find(params[:id]) - json_saved UpdateDonation.from_followup(donation, params[:donation]) - end + # put /nonprofits/:nonprofit_id/donations/:id + # update designation, dedication, or comment on a donation in the followup + def followup + nonprofit = Nonprofit.find(params[:nonprofit_id]) + donation = nonprofit.donations.find(params[:id]) + json_saved UpdateDonation.from_followup(donation, params[:donation]) + end - # this is a special, weird case - private + # this is a special, weird case + private - def current_campaign - if !@campaign && params[:donation] && params[:donation][:campaign_id] - @campaign = Campaign.where('id = ? ', params[:donation][:campaign_id]).first - end - return @campaign - end + def current_campaign + if !@campaign && params[:donation] && params[:donation][:campaign_id] + @campaign = Campaign.where('id = ? ', params[:donation][:campaign_id]).first + end + @campaign + end - def current_campaign_editor? - !params[:preview] && (current_nonprofit_user? || (current_campaign && current_role?(:campaign_editor, current_campaign.id)) || current_role?(:super_admin)) - end + def current_campaign_editor? + !params[:preview] && (current_nonprofit_user? || (current_campaign && current_role?(:campaign_editor, current_campaign.id)) || current_role?(:super_admin)) + end - def authenticate_campaign_editor! - unless current_campaign_editor? - block_with_sign_in 'You need to be a campaign editor to do that.' - end - end - end + def authenticate_campaign_editor! + unless current_campaign_editor? + block_with_sign_in 'You need to be a campaign editor to do that.' + end + end + end end diff --git a/app/controllers/nonprofits/email_lists_controller.rb b/app/controllers/nonprofits/email_lists_controller.rb index 25a0d8fb..4f4dcc13 100644 --- a/app/controllers/nonprofits/email_lists_controller.rb +++ b/app/controllers/nonprofits/email_lists_controller.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class EmailListsController < ApplicationController - include Controllers::NonprofitHelper + class EmailListsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! - def index - render_json{ Qx.fetch(:email_lists, nonprofit_id: params[:nonprofit_id]) } - end + def index + render_json { Qx.fetch(:email_lists, nonprofit_id: params[:nonprofit_id]) } + end - def create - tag_master_ids = params['tag_masters'].values.map(&:to_i) - render_json{ InsertEmailLists.for_mailchimp(params[:nonprofit_id], tag_master_ids) } + def create + tag_master_ids = params['tag_masters'].values.map(&:to_i) + render_json { InsertEmailLists.for_mailchimp(params[:nonprofit_id], tag_master_ids) } + end end end -end diff --git a/app/controllers/nonprofits/imports_controller.rb b/app/controllers/nonprofits/imports_controller.rb index 9498680a..a50f296c 100644 --- a/app/controllers/nonprofits/imports_controller.rb +++ b/app/controllers/nonprofits/imports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ImportsController < ApplicationController @@ -6,15 +8,15 @@ module Nonprofits before_action :authenticate_nonprofit_user! # post /nonprofits/:nonprofit_id/imports def create - render_json{ - InsertImport.delay.from_csv_safe({ + render_json do + InsertImport.delay.from_csv_safe( nonprofit_id: params[:nonprofit_id], user_id: current_user.id, user_email: current_user.email, file_uri: params[:file_uri], header_matches: params[:header_matches] - }) - } + ) + end end end end diff --git a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb index 639c4bc0..ecc4ebee 100644 --- a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb +++ b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class MiscellaneousNpInfosController < ApplicationController @@ -12,18 +14,17 @@ module Nonprofits render_json { FetchMiscellaneousNpInfo.fetch(params[:nonprofit_id]) } end end - end def update respond_to do |format| - format.json { - render_json { + format.json do + render_json do update = UpdateMiscellaneousNpInfo.update(params[:nonprofit_id], params[:miscellaneous_np_info]) - #flash[:notice] = "Your Miscellaneous Settings have been saved" + # flash[:notice] = "Your Miscellaneous Settings have been saved" update - } - } + end + end end end end diff --git a/app/controllers/nonprofits/nonprofit_keys_controller.rb b/app/controllers/nonprofits/nonprofit_keys_controller.rb index 7a247fe5..28fdb3f9 100644 --- a/app/controllers/nonprofits/nonprofit_keys_controller.rb +++ b/app/controllers/nonprofits/nonprofit_keys_controller.rb @@ -1,38 +1,39 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class NonprofitKeysController < ApplicationController - include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + class NonprofitKeysController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_user! - # get /nonprofits/:nonprofit_id/nonprofit_keys - # pass in the :select query param, which is the name of the column of the specific token you want - def index - render_json{QueryNonprofitKeys.get_key(current_nonprofit.id, params[:select])} - end - - # Redirects to the mailchimp OAuth2 landing page, first setting the nonprofit id in the session - # GET /nonprofits/:nonprofit_id/nonprofit_keys/mailchimp_login - def mailchimp_login - session[:current_mailchimp_nonprofit_id] = current_nonprofit.id - redirect_to "https://login.mailchimp.com/oauth2/authorize?response_type=code&client_id=#{ENV['MAILCHIMP_OAUTH_CLIENT_ID']}" - end - - # After the user OAuths mailchimp, they are redirected to /mailchimp-landing - # This action then redirects them back to /settings - # GET /mailchimp-landing - def mailchimp_landing - @nonprofit = Nonprofit.find(session[:current_mailchimp_nonprofit_id]) - session.delete(:current_mailchimp_nonprofit_id) - begin - session[:mailchimp_access_token] = InsertNonprofitKeys.insert_mailchimp_access_token(@nonprofit.id, params[:code]) - rescue Exception => e - flash[:notice] = "Unable to connect to your Mailchimp account, please try again. (Error: #{e})" - redirect_to '/settings' - return + # get /nonprofits/:nonprofit_id/nonprofit_keys + # pass in the :select query param, which is the name of the column of the specific token you want + def index + render_json { QueryNonprofitKeys.get_key(current_nonprofit.id, params[:select]) } end - redirect_to nonprofits_supporters_path @nonprofit, 'show-modal' => 'mailchimpSettingsModal' - end -end + # Redirects to the mailchimp OAuth2 landing page, first setting the nonprofit id in the session + # GET /nonprofits/:nonprofit_id/nonprofit_keys/mailchimp_login + def mailchimp_login + session[:current_mailchimp_nonprofit_id] = current_nonprofit.id + redirect_to "https://login.mailchimp.com/oauth2/authorize?response_type=code&client_id=#{ENV['MAILCHIMP_OAUTH_CLIENT_ID']}" + end + + # After the user OAuths mailchimp, they are redirected to /mailchimp-landing + # This action then redirects them back to /settings + # GET /mailchimp-landing + def mailchimp_landing + @nonprofit = Nonprofit.find(session[:current_mailchimp_nonprofit_id]) + session.delete(:current_mailchimp_nonprofit_id) + begin + session[:mailchimp_access_token] = InsertNonprofitKeys.insert_mailchimp_access_token(@nonprofit.id, params[:code]) + rescue Exception => e + flash[:notice] = "Unable to connect to your Mailchimp account, please try again. (Error: #{e})" + redirect_to '/settings' + return + end + redirect_to nonprofits_supporters_path @nonprofit, 'show-modal' => 'mailchimpSettingsModal' + end + end end diff --git a/app/controllers/nonprofits/payments_controller.rb b/app/controllers/nonprofits/payments_controller.rb index f54701cc..e579fad9 100644 --- a/app/controllers/nonprofits/payments_controller.rb +++ b/app/controllers/nonprofits/payments_controller.rb @@ -1,32 +1,33 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class PaymentsController < ApplicationController - include Controllers::NonprofitHelper + class PaymentsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! - - # get /nonprofit/:nonprofit_id/payments - def index - @nonprofit = current_nonprofit - respond_to do |format| - format.html do + # get /nonprofit/:nonprofit_id/payments + def index + @nonprofit = current_nonprofit + respond_to do |format| + format.html do @panels_layout = true end - format.json do - @response = QueryPayments.full_search(params[:nonprofit_id], params) + format.json do + @response = QueryPayments.full_search(params[:nonprofit_id], params) render json: @response, status: :ok - end - end - end # def index + end + end + end # def index def export begin @nonprofit = current_nonprofit @user = current_user_id - ExportPayments::initiate_export(@nonprofit.id, params, @user) - rescue => e + ExportPayments.initiate_export(@nonprofit.id, params, @user) + rescue StandardError => e e end if e.nil? @@ -37,10 +38,10 @@ module Nonprofits end end - def show - @nonprofit = current_nonprofit - @payment = @nonprofit.payments.find(params[:id]) - end # def show + def show + @nonprofit = current_nonprofit + @payment = @nonprofit.payments.find(params[:id]) + end # def show def update @payment = current_nonprofit.payments.find(params[:id]) @@ -68,11 +69,12 @@ module Nonprofits PaymentMailer.resend_donor_receipt(params[:id]) render json: {} end + # post /nonprofits/:nonprofit_id/payments/:id/resend_admin_receipt # pass user_id of the admin to send to def resend_admin_receipt PaymentMailer.resend_admin_receipt(params[:id], current_user.id) render json: {} end - end # class PaymentsController + end # class PaymentsController end # module Nonprofits diff --git a/app/controllers/nonprofits/payouts_controller.rb b/app/controllers/nonprofits/payouts_controller.rb index b9bbff0f..0d47912d 100644 --- a/app/controllers/nonprofits/payouts_controller.rb +++ b/app/controllers/nonprofits/payouts_controller.rb @@ -1,49 +1,50 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class PayoutsController < ApplicationController - include Controllers::NonprofitHelper + class PayoutsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_admin!, only: :create - before_action :authenticate_nonprofit_user!, only: [:index, :show] + before_action :authenticate_nonprofit_admin!, only: :create + before_action :authenticate_nonprofit_user!, only: %i[index show] - def create - payout = InsertPayout.with_stripe(current_nonprofit.id, { - stripe_account_id: current_nonprofit.stripe_account_id, - email: current_user.email, - user_ip: current_user.current_sign_in_ip, - bank_name: current_nonprofit.bank_account.name - }, {before_date: params[:before_date]}) + def create + payout = InsertPayout.with_stripe(current_nonprofit.id, { + stripe_account_id: current_nonprofit.stripe_account_id, + email: current_user.email, + user_ip: current_user.current_sign_in_ip, + bank_name: current_nonprofit.bank_account.name + }, before_date: params[:before_date]) - if payout['failure_message'].present? - flash[:notice] = "The payout failed: #{payout['failure_message']}" - render json: payout, status: :unprocessable_entity - else - flash[:notice] = 'We successfully submitted your payout! View status and receipts below.' - render json: payout, status: :ok - end - end - - def index - @nonprofit = Nonprofit.find(params[:nonprofit_id]) - @payouts = @nonprofit.payouts.order('created_at DESC') - balances = QueryPayments.nonprofit_balances(params[:nonprofit_id]) - @available_total = balances['available_gross'] - @pending_total = balances['pending_gross'] - @can_make_payouts = @nonprofit.can_make_payouts - end - - # get /nonprofits/:nonprofit_id/payouts/:id - def show - payout = current_nonprofit.payouts.find(params[:id]) - respond_to do |format| - format.json{render json: payout} - format.csv do - payments = QueryPayments.for_payout(params[:nonprofit_id], params[:id]) - filename = "payout-#{payout.created_at.strftime("%m-%d-%Y")}" - send_data(Format::Csv.from_vectors(payments), filename: "#{filename}.csv") + if payout['failure_message'].present? + flash[:notice] = "The payout failed: #{payout['failure_message']}" + render json: payout, status: :unprocessable_entity + else + flash[:notice] = 'We successfully submitted your payout! View status and receipts below.' + render json: payout, status: :ok end end - end -end + def index + @nonprofit = Nonprofit.find(params[:nonprofit_id]) + @payouts = @nonprofit.payouts.order('created_at DESC') + balances = QueryPayments.nonprofit_balances(params[:nonprofit_id]) + @available_total = balances['available_gross'] + @pending_total = balances['pending_gross'] + @can_make_payouts = @nonprofit.can_make_payouts + end + + # get /nonprofits/:nonprofit_id/payouts/:id + def show + payout = current_nonprofit.payouts.find(params[:id]) + respond_to do |format| + format.json { render json: payout } + format.csv do + payments = QueryPayments.for_payout(params[:nonprofit_id], params[:id]) + filename = "payout-#{payout.created_at.strftime('%m-%d-%Y')}" + send_data(Format::Csv.from_vectors(payments), filename: "#{filename}.csv") + end + end + end + end end diff --git a/app/controllers/nonprofits/recurring_donations_controller.rb b/app/controllers/nonprofits/recurring_donations_controller.rb index c35ea796..35be402d 100644 --- a/app/controllers/nonprofits/recurring_donations_controller.rb +++ b/app/controllers/nonprofits/recurring_donations_controller.rb @@ -1,94 +1,95 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class RecurringDonationsController < ApplicationController - include Controllers::NonprofitHelper + class RecurringDonationsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] - # get /nonprofits/:nonprofit_id/recurring_donations - def index - @nonprofit = current_nonprofit - @panels_layout = true - respond_to do |format| - format.html - format.json do - # set dashboard params include externally active and failed - #TODO move into javascript - params[:active] = true + # get /nonprofits/:nonprofit_id/recurring_donations + def index + @nonprofit = current_nonprofit + @panels_layout = true + respond_to do |format| + format.html + format.json do + # set dashboard params include externally active and failed + # TODO move into javascript + params[:active] = true - render json: QueryRecurringDonations.full_list(params[:nonprofit_id], params) + render json: QueryRecurringDonations.full_list(params[:nonprofit_id], params) + end end - end - end - - def export - begin - @nonprofit = current_nonprofit - @user = current_user_id - #TODO move into javascript - if params.key?(:active_and_not_failed) - params.delete(:active) if params.key?(:active) - params.delete(:failed) if params.key?(:failed) - end - - [:active_and_not_failed, :active, :failed].each do |k| - if (params.key?(k)) - params[k] = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(params[k]) - end - end - - params[:root_url] = root_url - - ExportRecurringDonations::initiate_export(@nonprofit.id, params, current_user.id) - rescue => e - e - end - if e.nil? - flash[:notice] = "Your export was successfully initiated and you'll be emailed at #{current_user.email} as soon as it's available. Feel free to use the site in the meantime." - render json: {}, status: :ok - else - render json: e, status: :ok - end - end - - def show - @recurring_donation = current_recurring_donation - respond_to {|format| format.json} - end - - def destroy - UpdateRecurringDonations.cancel(params[:id], current_user.email) - json_saved current_recurring_donation - end - - def update - json_saved UpdateRecurringDonations - .update(current_recurring_donation, params[:recurring_donation]) - end - - # post /nonprofits/:nonprofit_id/recurring_donations - def create - if params[:recurring_donation][:token] - render_json{ InsertRecurringDonation.with_stripe(params[:recurring_donation]) } - elsif params[:recurring_donation][:direct_debit_detail_id] - render JsonResp.new(params[:recurring_donation]){|data| - requires(:amount).as_int - requires(:supporter_id, :nonprofit_id, :direct_debit_detail_id).as_int - optional(:dedication, :designation).as_string - optional(:campaign_id, :event_id).as_int - }.when_valid{|data| - InsertRecurringDonation.with_sepa(data) - } - else - render json: {}, status: 422 end - end -private + def export + begin + @nonprofit = current_nonprofit + @user = current_user_id + # TODO: move into javascript + if params.key?(:active_and_not_failed) + params.delete(:active) if params.key?(:active) + params.delete(:failed) if params.key?(:failed) + end - def current_recurring_donation - @recurring_donation ||= current_nonprofit.recurring_donations.find params[:id] - end + %i[active_and_not_failed active failed].each do |k| + if params.key?(k) + params[k] = ActiveRecord::ConnectionAdapters::Column.value_to_boolean(params[k]) + end + end -end + params[:root_url] = root_url + + ExportRecurringDonations.initiate_export(@nonprofit.id, params, current_user.id) + rescue StandardError => e + e + end + if e.nil? + flash[:notice] = "Your export was successfully initiated and you'll be emailed at #{current_user.email} as soon as it's available. Feel free to use the site in the meantime." + render json: {}, status: :ok + else + render json: e, status: :ok + end + end + + def show + @recurring_donation = current_recurring_donation + respond_to { |format| format.json } + end + + def destroy + UpdateRecurringDonations.cancel(params[:id], current_user.email) + json_saved current_recurring_donation + end + + def update + json_saved UpdateRecurringDonations + .update(current_recurring_donation, params[:recurring_donation]) + end + + # post /nonprofits/:nonprofit_id/recurring_donations + def create + if params[:recurring_donation][:token] + render_json { InsertRecurringDonation.with_stripe(params[:recurring_donation]) } + elsif params[:recurring_donation][:direct_debit_detail_id] + render JsonResp.new(params[:recurring_donation]) do |_data| + requires(:amount).as_int + requires(:supporter_id, :nonprofit_id, :direct_debit_detail_id).as_int + optional(:dedication, :designation).as_string + optional(:campaign_id, :event_id).as_int + end.when_valid do |data| + InsertRecurringDonation.with_sepa(data) + end + else + render json: {}, status: 422 + end + end + + private + + def current_recurring_donation + @recurring_donation ||= current_nonprofit.recurring_donations.find params[:id] + end + end end diff --git a/app/controllers/nonprofits/refunds_controller.rb b/app/controllers/nonprofits/refunds_controller.rb index a06f1290..85c35fac 100644 --- a/app/controllers/nonprofits/refunds_controller.rb +++ b/app/controllers/nonprofits/refunds_controller.rb @@ -1,21 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class RefundsController < ApplicationController - include Controllers::NonprofitHelper + class RefundsController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + before_action :authenticate_nonprofit_user! - # post /charges/:charge_id/refunds - def create - charge = Qx.select("*").from("charges").where(id: params[:charge_id]).execute.first - params[:refund][:user_id] = current_user.id - render_json{ InsertRefunds.with_stripe(charge, params['refund']) } - end + # post /charges/:charge_id/refunds + def create + charge = Qx.select('*').from('charges').where(id: params[:charge_id]).execute.first + params[:refund][:user_id] = current_user.id + render_json { InsertRefunds.with_stripe(charge, params['refund']) } + end - def index - charge = current_nonprofit.charges.find(params[:charge_id]) - @refunds = charge.refunds - end - end + def index + charge = current_nonprofit.charges.find(params[:charge_id]) + @refunds = charge.refunds + end + end end - diff --git a/app/controllers/nonprofits/reports_controller.rb b/app/controllers/nonprofits/reports_controller.rb index 9dd6ca5c..c3c28646 100644 --- a/app/controllers/nonprofits/reports_controller.rb +++ b/app/controllers/nonprofits/reports_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ReportsController < ApplicationController @@ -8,7 +10,7 @@ module Nonprofits respond_to do |format| format.csv do filename = "end-of-year-report-#{params[:year]}.csv" - data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year]}) + data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], year: params[:year]) send_data(Format::Csv.from_array(data), filename: filename) end end @@ -18,17 +20,15 @@ module Nonprofits respond_to do |format| format.csv do name_description = nil - if (params[:year]) + if params[:year] name_description = params[:year] - elsif (params[:start]) + elsif params[:start] name_description = "from-#{params[:start]}" - if (params[:end]) - name_description += "-to-#{params[:end]}" - end + name_description += "-to-#{params[:end]}" if params[:end] end filename = "aggregate-report-#{name_description}.csv" - data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], {:year => params[:year], :start => params[:start], :end => params[:end]}) + data = QuerySupporters.year_aggregate_report(params[:nonprofit_id], year: params[:year], start: params[:start], end: params[:end]) send_data(Format::Csv.from_array(data), filename: filename) end end diff --git a/app/controllers/nonprofits/supporter_emails_controller.rb b/app/controllers/nonprofits/supporter_emails_controller.rb index fcd39683..1256f12d 100644 --- a/app/controllers/nonprofits/supporter_emails_controller.rb +++ b/app/controllers/nonprofits/supporter_emails_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class SupporterEmailsController < ApplicationController @@ -7,19 +9,19 @@ module Nonprofits def create if params[:selecting_all] ids = QuerySupporters.full_filter_expr(params[:nonprofit_id], params[:query]) - .select("supporters.id") - .execute(format: 'csv')[1..-1].flatten + .select('supporters.id') + .execute(format: 'csv')[1..-1].flatten elsif params[:supporter_ids] ids = params[:supporter_ids] end if ids.nil? || ids.empty? - render json: {errors: 'Supporters not found'}, status: :unprocessable_entity + render json: { errors: 'Supporters not found' }, status: :unprocessable_entity return end DelayedJobHelper.enqueue_job(EmailSupporters, :deliver, [ids, params[:supporter_email]]) - render json: {count: ids.count}, status: :ok + render json: { count: ids.count }, status: :ok end def gmail @@ -29,4 +31,3 @@ module Nonprofits end end end - diff --git a/app/controllers/nonprofits/supporter_notes_controller.rb b/app/controllers/nonprofits/supporter_notes_controller.rb index 3073033b..9e086409 100644 --- a/app/controllers/nonprofits/supporter_notes_controller.rb +++ b/app/controllers/nonprofits/supporter_notes_controller.rb @@ -1,27 +1,28 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class SupporterNotesController < ApplicationController - include Controllers::NonprofitHelper + class SupporterNotesController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] - # post /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes - def create - params[:supporter_note][:user_id] ||= current_user && current_user.id - render_json{ InsertSupporterNotes.create([params[:supporter_note]]) } - end + # post /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes + def create + params[:supporter_note][:user_id] ||= current_user&.id + render_json { InsertSupporterNotes.create([params[:supporter_note]]) } + end - # put /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id - def update - params[:supporter_note][:user_id] ||= current_user && current_user.id - params[:supporter_note][:id] = params[:id] - render_json{ UpdateSupporterNotes.update(params[:supporter_note]) } - end - - # delete /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id - def destroy - render_json{ UpdateSupporterNotes.delete(params[:id]) } - end + # put /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id + def update + params[:supporter_note][:user_id] ||= current_user&.id + params[:supporter_note][:id] = params[:id] + render_json { UpdateSupporterNotes.update(params[:supporter_note]) } + end -end + # delete /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id + def destroy + render_json { UpdateSupporterNotes.delete(params[:id]) } + end + end end diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index a81fbe9a..31684107 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -1,114 +1,114 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits -class SupportersController < ApplicationController - include Controllers::NonprofitHelper + class SupportersController < ApplicationController + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user!, except: [:new, :create] - #before_action(except: [:create, :mailchimp_landing]){authenticate_min_nonprofit_plan(2)} + before_action :authenticate_nonprofit_user!, except: %i[new create] + # before_action(except: [:create, :mailchimp_landing]){authenticate_min_nonprofit_plan(2)} - # get /nonprofit/:nonprofit_id/supporters - def index - @panels_layout = true - @nonprofit = current_nonprofit - respond_to do |format| - format.html - format.json do - render json: QuerySupporters.full_search(params[:nonprofit_id], params) - end - - format.csv do - file_date = Date.today.strftime("%m-%d-%Y") - supporters = QuerySupporters.for_export(params[:nonprofit_id], params) - send_data(Format::Csv.from_vectors(supporters), filename: "supporters-#{file_date}.csv") - end - end - end - - def export - begin + # get /nonprofit/:nonprofit_id/supporters + def index + @panels_layout = true @nonprofit = current_nonprofit - @user = current_user_id - ExportSupporters::initiate_export(@nonprofit.id, params, @user) - rescue => e - e + respond_to do |format| + format.html + format.json do + render json: QuerySupporters.full_search(params[:nonprofit_id], params) + end + + format.csv do + file_date = Date.today.strftime('%m-%d-%Y') + supporters = QuerySupporters.for_export(params[:nonprofit_id], params) + send_data(Format::Csv.from_vectors(supporters), filename: "supporters-#{file_date}.csv") + end + end end - if e.nil? - flash[:notice] = "Your export was successfully initiated and you'll be emailed at #{current_user.email} as soon as it's available. Feel free to use the site in the meantime." - render json: {}, status: :ok - else - render json: e, status: :ok + + def export + begin + @nonprofit = current_nonprofit + @user = current_user_id + ExportSupporters.initiate_export(@nonprofit.id, params, @user) + rescue StandardError => e + e + end + if e.nil? + flash[:notice] = "Your export was successfully initiated and you'll be emailed at #{current_user.email} as soon as it's available. Feel free to use the site in the meantime." + render json: {}, status: :ok + else + render json: e, status: :ok + end end - end - def index_metrics - render_json do - QuerySupporters.full_search_metrics(params[:nonprofit_id], params) + def index_metrics + render_json do + QuerySupporters.full_search_metrics(params[:nonprofit_id], params) + end end - end - def show - render json: {data: QuerySupporters.for_crm_profile(params[:nonprofit_id], [params[:id]]).first} - end - - def email_address - render json: Supporter.find(params[:supporter_id]).email - end - - def full_contact - fc = FullContactInfo.where("supporter_id=#{params[:supporter_id]}").first - if fc - render json: {full_contact: QueryFullContactInfos.fetch_associated_tables(fc.id )} - else - render json: {full_contact: nil} + def show + render json: { data: QuerySupporters.for_crm_profile(params[:nonprofit_id], [params[:id]]).first } end - end - def info_card - render json: QuerySupporters.for_info_card(params[:supporter_id]) - end - - - # post /nonprofits/:nonprofit_id/supporters - def create - render_json{ InsertSupporter.create_or_update(params[:nonprofit_id], params[:supporter]) } - end - - # put /nonprofits/:nonprofit_id/supporters/:id - def update - @supporter = current_nonprofit.supporters.find(params[:id]) - json_saved UpdateSupporter.from_info(@supporter, params[:supporter]) - end - - def bulk_delete - if params[:selecting_all] - supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select("supporters.id").execute.map{|h| h['id']} - else - supporter_ids = params[:supporter_ids]. map(&:to_i) + def email_address + render json: Supporter.find(params[:supporter_id]).email end - render_json {UpdateSupporter.bulk_delete(current_nonprofit.id, supporter_ids ) } - end - # get /nonprofits/:nonprofit_id/supporters/merge_data - # returns the info required to merge two supporters - def merge_data - render json: QuerySupporters.merge_data(params[:ids]) - end + def full_contact + fc = FullContactInfo.where("supporter_id=#{params[:supporter_id]}").first + if fc + render json: { full_contact: QueryFullContactInfos.fetch_associated_tables(fc.id) } + else + render json: { full_contact: nil } + end + end - # post /nonprofits/:nonprofit_id/supporters/merge - def merge - render JsonResp.new(params){|params| - requires(:supporter) - requires(:nonprofit_id).as_int - requires(:supporter_ids).as_array - }.when_valid{|params| - params[:supporter][:nonprofit_id] = params[:nonprofit_id] - MergeSupporters.selected(params[:supporter], params[:supporter_ids], params[:nonprofit_id], current_user.id) - } - end + def info_card + render json: QuerySupporters.for_info_card(params[:supporter_id]) + end - # def new - # @nonprofit = current_nonprofit - # end + # post /nonprofits/:nonprofit_id/supporters + def create + render_json { InsertSupporter.create_or_update(params[:nonprofit_id], params[:supporter]) } + end -end + # put /nonprofits/:nonprofit_id/supporters/:id + def update + @supporter = current_nonprofit.supporters.find(params[:id]) + json_saved UpdateSupporter.from_info(@supporter, params[:supporter]) + end + + def bulk_delete + if params[:selecting_all] + supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select('supporters.id').execute.map { |h| h['id'] } + else + supporter_ids = params[:supporter_ids]. map(&:to_i) + end + render_json { UpdateSupporter.bulk_delete(current_nonprofit.id, supporter_ids) } + end + + # get /nonprofits/:nonprofit_id/supporters/merge_data + # returns the info required to merge two supporters + def merge_data + render json: QuerySupporters.merge_data(params[:ids]) + end + + # post /nonprofits/:nonprofit_id/supporters/merge + def merge + render JsonResp.new(params) do |_params| + requires(:supporter) + requires(:nonprofit_id).as_int + requires(:supporter_ids).as_array + end.when_valid do |params| + params[:supporter][:nonprofit_id] = params[:nonprofit_id] + MergeSupporters.selected(params[:supporter], params[:supporter_ids], params[:nonprofit_id], current_user.id) + end + end + + # def new + # @nonprofit = current_nonprofit + # end + end end diff --git a/app/controllers/nonprofits/tag_joins_controller.rb b/app/controllers/nonprofits/tag_joins_controller.rb index f499f65c..0a9d2a42 100644 --- a/app/controllers/nonprofits/tag_joins_controller.rb +++ b/app/controllers/nonprofits/tag_joins_controller.rb @@ -1,36 +1,32 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits - class TagJoinsController < ApplicationController - include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_user! + class TagJoinsController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_user! def index render_json do - {data: QuerySupporters.tag_joins(params['nonprofit_id'], params['supporter_id'])} + { data: QuerySupporters.tag_joins(params['nonprofit_id'], params['supporter_id']) } end end - # used for modify a single supporter's tags or a group of - # selected supporters' tags or all supporters' tags - def modify - + # used for modify a single supporter's tags or a group of + # selected supporters' tags or all supporters' tags + def modify if params[:selecting_all] - supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select("supporters.id").execute.map{|h| h['id']} + supporter_ids = QuerySupporters.full_filter_expr(current_nonprofit.id, params[:query]).select('supporters.id').execute.map { |h| h['id'] } else supporter_ids = params[:supporter_ids]. map(&:to_i) end - render InsertTagJoins.in_bulk(current_nonprofit.id, current_user.profile.id, supporter_ids, params[:tags]) + render InsertTagJoins.in_bulk(current_nonprofit.id, current_user.profile.id, supporter_ids, params[:tags]) + end - - - end - - def destroy - supporter = current_nonprofit.supporters.find(params[:supporter_id]) - supporter.tag_joins.find(params[:id]).destroy - render json: {}, status: :ok - end - - end + def destroy + supporter = current_nonprofit.supporters.find(params[:supporter_id]) + supporter.tag_joins.find(params[:id]).destroy + render json: {}, status: :ok + end + end end - diff --git a/app/controllers/nonprofits/tag_masters_controller.rb b/app/controllers/nonprofits/tag_masters_controller.rb index 6abc8a7a..50858208 100644 --- a/app/controllers/nonprofits/tag_masters_controller.rb +++ b/app/controllers/nonprofits/tag_masters_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class TagMastersController < ApplicationController @@ -5,14 +7,14 @@ module Nonprofits before_action :authenticate_nonprofit_user! def index - render json: {data: - Qx.select('id', 'name', 'created_at') + render json: { data: + Qx.select('id', 'name', 'created_at') .from('tag_masters') .where( ['tag_masters.nonprofit_id = $id', id: current_nonprofit.id], - ["coalesce(deleted, FALSE) = FALSE"]) - .execute - } + ['coalesce(deleted, FALSE) = FALSE'] + ) + .execute } end def create @@ -25,7 +27,5 @@ module Nonprofits tag_master.tag_joins.destroy_all render json: {}, status: :ok end - end end - diff --git a/app/controllers/nonprofits/trackings_controller.rb b/app/controllers/nonprofits/trackings_controller.rb index 208e64b7..8a9ca5ce 100644 --- a/app/controllers/nonprofits/trackings_controller.rb +++ b/app/controllers/nonprofits/trackings_controller.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class TrackingsController < ApplicationController # POST /nonprofits/:nonprofit_id/tracking def create - render JsonResp.new(params){|data| + render JsonResp.new(params) do |_data| requires(:donation_id).as_int optional(:utm_campaign, :utm_content, :utm_medium, :utm_source).as_string - }.when_valid{|data| + end.when_valid do |_data| InsertTracking.create(params) - } + end end end end diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index 55aa04ab..47250a89 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -1,67 +1,69 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later - class NonprofitsController < ApplicationController - include Controllers::NonprofitHelper +class NonprofitsController < ApplicationController + include Controllers::NonprofitHelper - helper_method :current_nonprofit_user? - before_action :authenticate_nonprofit_user!, only: [:dashboard, :dashboard_metrics, :dashboard_todos, :payment_history, :profile_todos, :recurring_donation_stats, :update, :verify_identity] - before_action :authenticate_super_admin!, only: [:destroy] + helper_method :current_nonprofit_user? + before_action :authenticate_nonprofit_user!, only: %i[dashboard dashboard_metrics dashboard_todos payment_history profile_todos recurring_donation_stats update verify_identity] + before_action :authenticate_super_admin!, only: [:destroy] - # get /nonprofits/:id - # get /:state_code/:city/:name - def show + # get /nonprofits/:id + # get /:state_code/:city/:name + def show if !current_nonprofit.published && !current_role?(:super_admin) - block_with_sign_in + block_with_sign_in return end - @nonprofit = current_nonprofit - @url = Format::Url.concat(root_url, @nonprofit.url) - @supporters = @nonprofit.supporters.not_deleted - @profiles = @nonprofit.profiles.order('total_raised DESC').limit(5).includes(:user).uniq + @nonprofit = current_nonprofit + @url = Format::Url.concat(root_url, @nonprofit.url) + @supporters = @nonprofit.supporters.not_deleted + @profiles = @nonprofit.profiles.order('total_raised DESC').limit(5).includes(:user).uniq events = @nonprofit.events.not_deleted.order('start_datetime desc') campaigns = @nonprofit.campaigns.not_deleted.not_a_child.order('created_at desc') - @events = events.upcoming - @any_past_events = events.past.any? - @active_campaigns = campaigns.active - @any_past_campaigns = campaigns.past.any? + @events = events.upcoming + @any_past_events = events.past.any? + @active_campaigns = campaigns.active + @any_past_campaigns = campaigns.past.any? - @nonprofit_background_image = FetchBackgroundImage.with_model(@nonprofit) + @nonprofit_background_image = FetchBackgroundImage.with_model(@nonprofit) - respond_to do |format| - format.html - format.json {@nonprofit} - end - end + respond_to do |format| + format.html + format.json { @nonprofit } + end + end def recurring_donation_stats render json: QueryRecurringDonations.overall_stats(params[:nonprofit_id]) end - def profile_todos - render json: FetchTodoStatus.for_profile(current_nonprofit) - end + def profile_todos + render json: FetchTodoStatus.for_profile(current_nonprofit) + end - def dashboard_todos - render json: FetchTodoStatus.for_dashboard(current_nonprofit) - end + def dashboard_todos + render json: FetchTodoStatus.for_dashboard(current_nonprofit) + end - def create + def create current_user ||= User.find(params[:user_id]) - json_saved Nonprofit.register(current_user, params[:nonprofit]) - end + json_saved Nonprofit.register(current_user, params[:nonprofit]) + end - def update - flash[:notice] = 'Update successful!' + def update + flash[:notice] = 'Update successful!' current_nonprofit.update_attributes params[:nonprofit].except(:verification_status) - json_saved current_nonprofit - end + json_saved current_nonprofit + end - def destroy - current_nonprofit.destroy - flash[:notice] = 'Nonprofit removed' - render json: {} - end + def destroy + current_nonprofit.destroy + flash[:notice] = 'Nonprofit removed' + render json: {} + end # get /nonprofits/:id/donate def donate @@ -69,18 +71,18 @@ @referer = params[:origin] || request.env['HTTP_REFERER'] @campaign = current_nonprofit.campaigns.find_by_id(params[:campaign_id]) if params[:campaign_id] @countries_translations = countries_list(I18n.locale) - respond_to { |format| format.html{render layout: 'layouts/embed'} } + respond_to { |format| format.html { render layout: 'layouts/embed' } } end - def btn - @nonprofit = current_nonprofit - respond_to { |format| format.html{render layout: 'layouts/embed'} } - end + def btn + @nonprofit = current_nonprofit + respond_to { |format| format.html { render layout: 'layouts/embed' } } + end # get /nonprofits/:id/supporter_form def supporter_form - @nonprofit = current_nonprofit - respond_to { |format| format.html{render layout: 'layouts/embed'} } + @nonprofit = current_nonprofit + respond_to { |format| format.html { render layout: 'layouts/embed' } } end # post /nonprofits/:id/supporter_with_tag @@ -89,21 +91,21 @@ render json: InsertSupporter.with_tags_and_fields(@nonprofit.id, params[:supporter]) end - def dashboard - @nonprofit = current_nonprofit - respond_to { |format| format.html } - end + def dashboard + @nonprofit = current_nonprofit + respond_to { |format| format.html } + end - def dashboard_metrics - render json: Hamster::Hash[data: NonprofitMetrics.all_metrics(current_nonprofit.id)] - end + def dashboard_metrics + render json: Hamster::Hash[data: NonprofitMetrics.all_metrics(current_nonprofit.id)] + end - def payment_history + def payment_history render json: NonprofitMetrics.payment_history(params) - end + end - # put /nonprofits/:id/verify_identity - def verify_identity + # put /nonprofits/:id/verify_identity + def verify_identity if params[:legal_entity][:address] tos = { ip: current_user.current_sign_in_ip, @@ -111,8 +113,8 @@ user_agent: request.user_agent } end - render_json{ UpdateNonprofit.verify_identity(params[:nonprofit_id], params[:legal_entity], tos) } - end + render_json { UpdateNonprofit.verify_identity(params[:nonprofit_id], params[:legal_entity], tos) } + end def search render json: QueryNonprofits.by_search_string(params[:npo_name]) @@ -132,13 +134,12 @@ all_countries = ISO3166::Country.translations(locale) if Settings.intntl.all_countries - countries = all_countries.select{ |code, name| Settings.intntl.all_countries.include? code } - countries = countries.map{ |code, name| [code.upcase, name] }.sort{ |a, b| a[1] <=> b[1] } + countries = all_countries.select { |code, _name| Settings.intntl.all_countries.include? code } + countries = countries.map { |code, name| [code.upcase, name] }.sort_by { |a| a[1] } countries.push([Settings.intntl.other_country.upcase, I18n.t('nonprofits.donate.info.supporter.other_country')]) if Settings.intntl.other_country countries else - all_countries.map{ |code, name| [code.upcase, name] }.sort{ |a, b| a[1] <=> b[1] } + all_countries.map { |code, name| [code.upcase, name] }.sort_by { |a| a[1] } end end - end diff --git a/app/controllers/onboard_controller.rb b/app/controllers/onboard_controller.rb index 7e11316c..ceb7d782 100644 --- a/app/controllers/onboard_controller.rb +++ b/app/controllers/onboard_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class OnboardController < ApplicationController layout 'layouts/apified' def index diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index f171f546..a471a858 100755 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -1,63 +1,64 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ProfilesController < ApplicationController - helper_method :authenticate_profile_owner! - before_action :authenticate_profile_owner!, only: [:update, :fundraisers, :donations_history] + before_action :authenticate_profile_owner!, only: %i[update fundraisers donations_history] - # get /profiles/:id - # public profile - def show - @profile = Profile.find(params[:id]) - @profile_nonprofits = Psql.execute(Qexpr.new.select("DISTINCT nonprofits.*").from(:nonprofits).join(:supporters, "supporters.nonprofit_id=nonprofits.id AND supporters.profile_id=#{@profile.id}")) + # get /profiles/:id + # public profile + def show + @profile = Profile.find(params[:id]) + @profile_nonprofits = Psql.execute(Qexpr.new.select('DISTINCT nonprofits.*').from(:nonprofits).join(:supporters, "supporters.nonprofit_id=nonprofits.id AND supporters.profile_id=#{@profile.id}")) @campaigns = @profile.campaigns.published.includes(:nonprofit) - if @profile.anonymous? && current_user_id != @profile.user_id && !:super_admin - flash[:notice] = 'That user does not have a public profile.' - redirect_to(request.env["HTTP_REFERER"] || root_url) - return - end - end + if @profile.anonymous? && current_user_id != @profile.user_id && !:super_admin + flash[:notice] = 'That user does not have a public profile.' + redirect_to(request.env['HTTP_REFERER'] || root_url) + return + end + end - # get /profiles/:id/donations_history - def donations_history + # get /profiles/:id/donations_history + def donations_history validate - @profile = Profile.find(params[:id]) - @recurring_donations = @profile.recurring_donations.where(:active => true).includes(:nonprofit) - @donations = @profile.donations.includes(:nonprofit) - end + @profile = Profile.find(params[:id]) + @recurring_donations = @profile.recurring_donations.where(active: true).includes(:nonprofit) + @donations = @profile.donations.includes(:nonprofit) + end # get /profiles/:id/fundraisers def fundraisers validate current_user = Profile.find(params[:id]).user @profile = current_user.profile - @edited_campaigns = Campaign.where("profile_id=#{@profile.id}").order("end_datetime DESC") + @edited_campaigns = Campaign.where("profile_id=#{@profile.id}").order('end_datetime DESC') end # get /profiles/:id/events def events - render json: QueryEventMetrics.for_listings('profile', params[:id], params) + render json: QueryEventMetrics.for_listings('profile', params[:id], params) end - # put /profiles/:id - def update - if current_role?(:super_admin) # can update other profiles - @profile = Profile.find(params[:id]) - else - @profile = current_user.profile - end - @profile.update_attributes(params[:profile]) - json_saved @profile, 'Profile updated' - end + # put /profiles/:id + def update + @profile = if current_role?(:super_admin) # can update other profiles + Profile.find(params[:id]) + else + current_user.profile + end + @profile.update_attributes(params[:profile]) + json_saved @profile, 'Profile updated' + end private - def authenticate_profile_owner!() - if (!current_role?(:super_associate) && - !current_role?(:super_admin) && - (!current_user || - !current_user.profile || - current_user.profile.id != params[:id].to_i)) + def authenticate_profile_owner! + if !current_role?(:super_associate) && + !current_role?(:super_admin) && + (!current_user || + !current_user.profile || + current_user.profile.id != params[:id].to_i) block_with_sign_in end end diff --git a/app/controllers/recurring_donations_controller.rb b/app/controllers/recurring_donations_controller.rb index 3561dd65..bdf0e5ae 100644 --- a/app/controllers/recurring_donations_controller.rb +++ b/app/controllers/recurring_donations_controller.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RecurringDonationsController < ApplicationController - def edit @data = QueryRecurringDonations.fetch_for_edit params[:id] if @data && params[:t] == @data['recurring_donation']['edit_token'] @data['change_amount_suggestions'] = CalculateSuggestedAmounts.calculate(@data['recurring_donation']['amount']) @data['miscellaneous_np_info'] = FetchMiscellaneousNpInfo.fetch(@data['nonprofit']['id']) if @data['miscellaneous_np_info']['donate_again_url'].blank? - @data['miscellaneous_np_info']['donate_again_url'] = url_for(:controller => :nonprofits, :action=> :show, :id => @data['nonprofit']['id'], :only_path => false) + @data['miscellaneous_np_info']['donate_again_url'] = url_for(controller: :nonprofits, action: :show, id: @data['nonprofit']['id'], only_path: false) end respond_to do |format| format.html end else - flash[:notice] = "Unable to find donation. Please follow the exact link provided in your email" + flash[:notice] = 'Unable to find donation. Please follow the exact link provided in your email' redirect_to root_url end end @@ -21,7 +22,7 @@ class RecurringDonationsController < ApplicationController def destroy @data = QueryRecurringDonations.fetch_for_edit params[:id] if params[:edit_token] != @data['recurring_donation']['edit_token'] - render json: {error: 'Invalid token'}, status: :unprocessable_entity + render json: { error: 'Invalid token' }, status: :unprocessable_entity else updated = UpdateRecurringDonations.cancel(params[:id], current_user ? current_user.email : @data['supporter']['email']) render json: updated @@ -37,7 +38,7 @@ class RecurringDonationsController < ApplicationController data['recurring_donation'] = UpdateRecurringDonations.update_paydate(data['recurring_donation'], params[:paydate]) if params[:paydate] render json: data, status: data.is_a?(ValidationError) ? :unprocessable_entity : :ok else - render json: {error: 'Invalid token'}, status: :unprocessable_entity + render json: { error: 'Invalid token' }, status: :unprocessable_entity end end @@ -45,15 +46,14 @@ class RecurringDonationsController < ApplicationController rd = RecurringDonation.where('id = ?', params[:id]).first if rd && params[:edit_token] == rd['edit_token'] begin - amount_response = UpdateRecurringDonations.update_amount(rd, params[:token], params[:amount]) - flash[:notice] = "Your recurring donation amount has been successfully changed to $#{(amount_response.amount/100).to_i}" + amount_response = UpdateRecurringDonations.update_amount(rd, params[:token], params[:amount]) + flash[:notice] = "Your recurring donation amount has been successfully changed to $#{(amount_response.amount / 100).to_i}" render_json { amount_response } - rescue => e + rescue StandardError => e render_json { raise e } end else - render json: {error: 'Invalid token'}, status: :unprocessable_entity + render json: { error: 'Invalid token' }, status: :unprocessable_entity end end - end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index ff211e3b..fd734d65 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -1,23 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RolesController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper - before_action :authenticate_nonprofit_admin! + before_action :authenticate_nonprofit_admin! - def create - role = Role.create_for_nonprofit(params[:role][:name].to_sym, params[:role][:email], FetchNonprofit.with_params(params)) - json_saved role, "User successfully added!" - end + def create + role = Role.create_for_nonprofit(params[:role][:name].to_sym, params[:role][:email], FetchNonprofit.with_params(params)) + json_saved role, 'User successfully added!' + end - def destroy - role = Role.find(params[:id]) - roles = role.user.roles.where(host_id: params[:nonprofit_id], name: role.name) - unless roles.empty? - roles.destroy_all - flash[:notice] = 'User successfully removed' - render json: {} - else - render json: {:error => "We couldn't find that admin"}, :status => :unprocessable_entity - end - end + def destroy + role = Role.find(params[:id]) + roles = role.user.roles.where(host_id: params[:nonprofit_id], name: role.name) + if roles.empty? + render json: { error: "We couldn't find that admin" }, status: :unprocessable_entity + else + roles.destroy_all + flash[:notice] = 'User successfully removed' + render json: {} + end + end end diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index a0e60553..b35db2f4 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -1,31 +1,31 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SettingsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper - helper_method :current_nonprofit_user? - before_action :authenticate_user! + helper_method :current_nonprofit_user? + before_action :authenticate_user! - def index - if current_role?(:super_admin) && params[:nonprofit_id] - @nonprofit = Nonprofit.find(params[:nonprofit_id]) - elsif current_role?([:nonprofit_admin, :nonprofit_associate]) - @nonprofit = administered_nonprofit - end - - if current_role?(:super_admin) && params[:user_id] - @user = User.find_by_id(params[:user_id]) - elsif current_role?(:super_admin) && params[:user_email] - @user = User.find_by_email(params[:user_email]) - else - @user = current_user + def index + if current_role?(:super_admin) && params[:nonprofit_id] + @nonprofit = Nonprofit.find(params[:nonprofit_id]) + elsif current_role?(%i[nonprofit_admin nonprofit_associate]) + @nonprofit = administered_nonprofit end - @profile = @user.profile + @user = if current_role?(:super_admin) && params[:user_id] + User.find_by_id(params[:user_id]) + elsif current_role?(:super_admin) && params[:user_email] + User.find_by_email(params[:user_email]) + else + current_user + end - if @nonprofit - @miscellaneous_np_info = FetchMiscellaneousNpInfo.fetch(@nonprofit.id) - end - - end + @profile = @user.profile + if @nonprofit + @miscellaneous_np_info = FetchMiscellaneousNpInfo.fetch(@nonprofit.id) + end + end end diff --git a/app/controllers/static_controller.rb b/app/controllers/static_controller.rb index 6598e0f2..00e729ba 100644 --- a/app/controllers/static_controller.rb +++ b/app/controllers/static_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class StaticController < ApplicationController layout 'layouts/static' @@ -8,18 +10,17 @@ class StaticController < ApplicationController def ccs ccs_method = !Settings.ccs ? 'local_tar_gz' : Settings.ccs.ccs_method - if (ccs_method == 'local_tar_gz') + if ccs_method == 'local_tar_gz' temp_file = "#{Rails.root}/tmp/#{Time.current.to_i}.tar.gz" result = Kernel.system("git archive --format=tar.gz -o #{temp_file} HEAD") if result - send_file(temp_file, :type => "application/gzip") + send_file(temp_file, type: 'application/gzip') else render body: nil, status: 500 end - elsif (ccs_method == 'github') + elsif ccs_method == 'github' git_hash = File.read("#{Rails.root}/CCS_HASH") redirect_to "https://github.com/#{Settings.ccs.options.account}/#{Settings.ccs.options.repo}/tree/#{git_hash}" end - end end diff --git a/app/controllers/super_admins_controller.rb b/app/controllers/super_admins_controller.rb index 84ed07a5..cd444ceb 100644 --- a/app/controllers/super_admins_controller.rb +++ b/app/controllers/super_admins_controller.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SuperAdminsController < ApplicationController - layout "layouts/page" + layout 'layouts/page' before_action :authenticate_super_associate! - def index - end + def index; end def search_nonprofits render json: QueryNonprofits.for_admin(params) @@ -15,49 +16,47 @@ class SuperAdminsController < ApplicationController render json: QueryProfiles.for_admin(params) end - def search_fullcontact + def search_fullcontact begin result = FullContact.person(email: params[:search]) rescue Exception => e - result = '' + result = '' end render json: [result] end def resend_user_confirmation - ParamValidation.new(params || {}, { - profile_id: {:required => true, is_integer: true} - }) + ParamValidation.new(params || {}, + profile_id: { required: true, is_integer: true }) profile = Profile.includes(:user).where('id = ?', params[:profile_id]).first - unless (profile.user) - raise ArgumentError.new("#{params[:profile_id]} is a profile without a valid user") + unless profile.user + raise ArgumentError, "#{params[:profile_id]} is a profile without a valid user" end profile.user.send_confirmation_instructions - render json: {status: :ok} + render json: { status: :ok } end def recurring_donations_without_cards - odd_donations = QueryRecurringDonations::recurring_donations_without_cards + odd_donations = QueryRecurringDonations.recurring_donations_without_cards respond_to do |format| format.html format.csv do - csv_out = CSV.generate { |csv| + csv_out = CSV.generate do |csv| csv << ['supporter id', 'recurring donation id', 'rd created date', 'rd modified', 'donation id', 'donation card id', 'edit_token', 'nonprofit id', 'last charge succeeded id', 'last charge succeeded created at', 'last charge attempted id', 'last charge attempted created at', 'amount'] - odd_donations.each { |rd| + odd_donations.each do |rd| csv << [rd.supporter.id, rd.id, rd.created_at, rd.updated_at, rd.donation.id, rd.donation.card_id, rd.edit_token, rd.nonprofit.id, rd.most_recent_paid_charge.id, rd.most_recent_paid_charge.created_at, rd.most_recent_charge.id, rd.most_recent_charge.created_at, rd.amount] - } - } + end + end - - send_data(csv_out, filename: "recurring_donations_without_cards-#{Time.now.to_date()}.csv") + send_data(csv_out, filename: "recurring_donations_without_cards-#{Time.now.to_date}.csv") end end end @@ -65,17 +64,13 @@ class SuperAdminsController < ApplicationController def export_supporters_with_rds np = params[:np] ids = params[:ids] - results = QuerySupporters.for_export(np, {ids: ids}) - results[0].push("Management URLS") - results.drop(1).each {|row| - rds = Supporter.includes(:recurring_donations).find(row.last).recurring_donations.select{|rd| rd.active}.map{|rd| "* #{root_url}recurring_donations/#{rd.id}/edit?t=#{rd.edit_token}"}.join("\n") + results = QuerySupporters.for_export(np, ids: ids) + results[0].push('Management URLS') + results.drop(1).each do |row| + rds = Supporter.includes(:recurring_donations).find(row.last).recurring_donations.select(&:active).map { |rd| "* #{root_url}recurring_donations/#{rd.id}/edit?t=#{rd.edit_token}" }.join("\n") row.push(rds) - } + end - - send_data(Format::Csv.from_vectors(results), filename: "supporters_with_multiple_donations.csv") + send_data(Format::Csv.from_vectors(results), filename: 'supporters_with_multiple_donations.csv') end - - end - diff --git a/app/controllers/ticket_levels_controller.rb b/app/controllers/ticket_levels_controller.rb index 3e12ff56..1996fe8c 100644 --- a/app/controllers/ticket_levels_controller.rb +++ b/app/controllers/ticket_levels_controller.rb @@ -1,27 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketLevelsController < ApplicationController - include Controllers::EventHelper + include Controllers::EventHelper - before_action :authenticate_event_editor!, :except => [:index, :show] + before_action :authenticate_event_editor!, except: %i[index show] - def index + def index ev_id = current_event.id - render json: {data: QueryTicketLevels.with_event_id(ev_id, current_role?(:event_editor, ev_id) || current_role?(:super_admin) || current_role?(:nonprofit_admin, current_event.nonprofit_id))} - end + render json: { data: QueryTicketLevels.with_event_id(ev_id, current_role?(:event_editor, ev_id) || current_role?(:super_admin) || current_role?(:nonprofit_admin, current_event.nonprofit_id)) } + end - def show - render json: current_ticket_level - end + def show + render json: current_ticket_level + end - def create - ticket_level = current_event.ticket_levels.create params[:ticket_level] - json_saved ticket_level, 'Ticket level created!' - end + def create + ticket_level = current_event.ticket_levels.create params[:ticket_level] + json_saved ticket_level, 'Ticket level created!' + end - def update - current_ticket_level.update_attributes params[:ticket_level] - json_saved current_ticket_level, 'Ticket level updated' - end + def update + current_ticket_level.update_attributes params[:ticket_level] + json_saved current_ticket_level, 'Ticket level updated' + end # put /nonprofits/:nonprofit_id/events/:event_id/ticket_levels/update_order # Pass in {data: [{id: 1, order: 1}]} @@ -31,14 +33,13 @@ class TicketLevelsController < ApplicationController end def destroy - current_ticket_level.destroy - render json: {} - end + current_ticket_level.destroy + render json: {} + end -private - - def current_ticket_level - @ticket_level ||= current_event.ticket_levels.find params[:id] - end + private + def current_ticket_level + @ticket_level ||= current_event.ticket_levels.find params[:id] + end end diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 407961b0..a37b2b95 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketsController < ApplicationController - include Controllers::EventHelper + include Controllers::EventHelper - helper_method :current_event_admin?, :current_event_editor? - before_action :authenticate_event_editor!, :except => [:create, :add_note] + helper_method :current_event_admin?, :current_event_editor? + before_action :authenticate_event_editor!, except: %i[create add_note] before_action :authenticate_nonprofit_user!, only: [:delete_card_for_ticket] - # post /nonprofits/:nonprofit_id/events/:event_id/tickets - def create + # post /nonprofits/:nonprofit_id/events/:event_id/tickets + def create authenticate_event_editor! if params[:kind] == 'offsite' render_json do params[:current_user] = current_user @@ -18,29 +20,29 @@ class TicketsController < ApplicationController def update params[:ticket][:ticket_id] = params[:id] params[:ticket][:event_id] = params[:event_id] - render_json{ UpdateTickets.update(params[:ticket], current_user) } + render_json { UpdateTickets.update(params[:ticket], current_user) } end # Attendees dashboard - # get /nonprofits/:nonprofit_id/events/:event_id/tickets - def index - @panels_layout = true - @nonprofit = current_nonprofit - @event = current_event - respond_to do |format| - format.html + # get /nonprofits/:nonprofit_id/events/:event_id/tickets + def index + @panels_layout = true + @nonprofit = current_nonprofit + @event = current_event + respond_to do |format| + format.html format.csv do - file_date = Date.today.strftime("%m-%d-%Y") - filename = "tickets-#{file_date}" + file_date = Date.today.strftime('%m-%d-%Y') + filename = "tickets-#{file_date}" @tickets = QueryTickets.for_export(@event.id, params) - send_data(Format::Csv.from_vectors(@tickets), filename: "#{filename}.csv") + send_data(Format::Csv.from_vectors(@tickets), filename: "#{filename}.csv") end - format.json do + format.json do render json: QueryTickets.attendees_list(@event.id, params) - end - end - end + end + end + end # PUT nonprofits/:nonprofit_id/events/:event_id/tickets/:id/add_note def add_note diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb index 97812ee8..77601e37 100644 --- a/app/controllers/users/confirmations_controller.rb +++ b/app/controllers/users/confirmations_controller.rb @@ -1,41 +1,41 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Users::ConfirmationsController < Devise::ConfirmationsController + # get /confirm + def show + @user = User.confirm_by_token(params[:confirmation_token]) - # get /confirm - def show - @user = User.confirm_by_token(params[:confirmation_token]) - - if !@user.auto_generated || !@user.valid? - flash[:notice] = "We successfully confirmed your account" - redirect_to session[:donor_signup_url] || root_url - else + if !@user.auto_generated || !@user.valid? + flash[:notice] = 'We successfully confirmed your account' + redirect_to session[:donor_signup_url] || root_url + else respond_to do |format| format.html end - end - end + end + end def exists render json: User.find_by_email(params[:email]) end - # post /confirm - # set account password - def confirm - @user = User.find(params[:id]) + # post /confirm + # set account password + def confirm + @user = User.find(params[:id]) - if @user.valid? && @user.update_attributes(params[:user].except(:confirmation_token)) - flash[:notice] = "Your account is all set!" - sign_in @user - redirect_to session[:donor_signup_url] || root_url - else - session[:donor_signup_url] || root_url - #render :action => "show", :layout => 'layouts/embed' - end - end - - def is_confirmed - render json: {is_confirmed: User.find(params[:user_id]).confirmed?} + if @user.valid? && @user.update_attributes(params[:user].except(:confirmation_token)) + flash[:notice] = 'Your account is all set!' + sign_in @user + redirect_to session[:donor_signup_url] || root_url + else + session[:donor_signup_url] || root_url + # render :action => "show", :layout => 'layouts/embed' + end end + def is_confirmed + render json: { is_confirmed: User.find(params[:user_id]).confirmed? } + end end diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 24288ad8..1f516e7a 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Users::RegistrationsController < Devise::RegistrationsController respond_to :html, :json @@ -12,9 +14,9 @@ class Users::RegistrationsController < Devise::RegistrationsController user = User.register_donor!(params[:user]) if user.save sign_in user - render :json => user + render json: user else - render :json => user.errors.full_messages, :status => :unprocessable_entity + render json: user.errors.full_messages, status: :unprocessable_entity clean_up_passwords(user) end end @@ -33,7 +35,7 @@ class Users::RegistrationsController < Devise::RegistrationsController errs = current_user.errors.full_messages else success = false - errs = {:password => :incorrect} + errs = { password: :incorrect } end if success @@ -43,10 +45,10 @@ class Users::RegistrationsController < Devise::RegistrationsController flash[:notice] = 'Account updated!' end - sign_in(current_user, :bypass => true) - render :json => current_user + sign_in(current_user, bypass: true) + render json: current_user else - render :json => {:errors => errs}, :status => :unprocessable_entity + render json: { errors: errs }, status: :unprocessable_entity end end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index 4ffebf7e..0271dad8 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,37 +1,36 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Users::SessionsController < Devise::SessionsController - layout 'layouts/apified', only: :new - respond_to :json, only: :new + layout 'layouts/apified', only: :new + respond_to :json, only: :new def new @theme = 'minimal' super end - def create + def create @theme = 'minimal' - respond_to do |format| - format.json { - warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new") - render :status => 200, :json => { :status => "Success" } - } - end - end - - - # post /users/confirm_auth - # A simple action to confirm an entered password for a user who is already signed in - def confirm_auth - if current_user.valid_password?(params[:password]) - tok = SecureRandom.uuid - session[:pw_token] = tok - session[:pw_timestamp] = Time.current.to_s - render json: {token: tok}, status: :ok - else - render json: ["Incorrect password. Please enter your #{Settings.general.name} %> password."], status: :unprocessable_entity - end + respond_to do |format| + format.json do + warden.authenticate!(scope: resource_name, recall: "#{controller_path}#new") + render status: 200, json: { status: 'Success' } + end + end end - -end + # post /users/confirm_auth + # A simple action to confirm an entered password for a user who is already signed in + def confirm_auth + if current_user.valid_password?(params[:password]) + tok = SecureRandom.uuid + session[:pw_token] = tok + session[:pw_timestamp] = Time.current.to_s + render json: { token: tok }, status: :ok + else + render json: ["Incorrect password. Please enter your #{Settings.general.name} %> password."], status: :unprocessable_entity + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e2f981df..d6208207 100755 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -1,70 +1,71 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ApplicationHelper + def resource_name + :user + end - def resource_name - :user - end + def resource + @resource ||= User.new + end - def resource - @resource ||= User.new - end + def devise_mapping + @devise_mapping ||= Devise.mappings[:user] + end - def devise_mapping - @devise_mapping ||= Devise.mappings[:user] - end + def print_currency(cents, unit = 'EUR', sign = true) + dollars = cents.to_f / 100.0 + dollars = number_to_currency(dollars, unit: unit.to_s, precision: dollars.round == dollars ? 0 : 2) + dollars = dollars[1..-1] unless sign + dollars + end - def print_currency(cents, unit="EUR", sign=true) - - dollars = cents.to_f / 100.0 - dollars = number_to_currency(dollars, :unit => "#{unit}", :precision => (dollars.round == dollars) ? 0 : 2) - dollars = dollars[1..-1] if !sign - dollars - end + def print_percent(rate) + (rate.to_f * 100).round(2) + end - def print_percent(rate) - (rate.to_f * 100).round(2) - end + ## Dates - ## Dates + def simple_date(date_object, timezone = nil) + return '' if date_object.nil? - def simple_date date_object, timezone=nil - return '' if date_object.nil? - date_object = date_object.in_time_zone(timezone) if timezone - date_object.strftime("%m/%d/%Y") - end + date_object = date_object.in_time_zone(timezone) if timezone + date_object.strftime('%m/%d/%Y') + end - def simple_time time_object, timezone=nil - return '' if time_object.nil? - time_object = time_object.in_time_zone(timezone) if timezone - time_object.strftime("%l:%M%P") - end + def simple_time(time_object, timezone = nil) + return '' if time_object.nil? - def readable_date date_object - date_object.strftime("%B %d, %Y") - end + time_object = time_object.in_time_zone(timezone) if timezone + time_object.strftime('%l:%M%P') + end - def date_and_time date_object, timezone=nil - date_object = date_object.in_time_zone(timezone) if timezone - date_object.strftime("%m/%d/%Y %I:%M%P (%Z)") - end + def readable_date(date_object) + date_object.strftime('%B %d, %Y') + end - def us_states - [ ['Alabama', 'AL'], ['Alaska', 'AK'], ['Arizona', 'AZ'], ['Arkansas', 'AR'], ['California', 'CA'], ['Colorado', 'CO'], ['Connecticut', 'CT'], ['Delaware', 'DE'], ['District of Columbia', 'DC'], ['Florida', 'FL'], ['Georgia', 'GA'], ['Hawaii', 'HI'], ['Idaho', 'ID'], ['Illinois', 'IL'], ['Indiana', 'IN'], ['Iowa', 'IA'], ['Kansas', 'KS'], ['Kentucky', 'KY'], ['Louisiana', 'LA'], ['Maine', 'ME'], ['Maryland', 'MD'], ['Massachusetts', 'MA'], ['Michigan', 'MI'], ['Minnesota', 'MN'], ['Mississippi', 'MS'], ['Missouri', 'MO'], ['Montana', 'MT'], ['Nebraska', 'NE'], ['Nevada', 'NV'], ['New Hampshire', 'NH'], ['New Jersey', 'NJ'], ['New Mexico', 'NM'], ['New York', 'NY'], ['North Carolina', 'NC'], ['North Dakota', 'ND'], ['Ohio', 'OH'], ['Oklahoma', 'OK'], ['Oregon', 'OR'], ['Pennsylvania', 'PA'], ['Puerto Rico', 'PR'], ['Rhode Island', 'RI'], ['South Carolina', 'SC'], ['South Dakota', 'SD'], ['Tennessee', 'TN'], ['Texas', 'TX'], ['Utah', 'UT'], ['Vermont', 'VT'], ['Virginia', 'VA'], ['Washington', 'WA'], ['West Virginia', 'WV'], ['Wisconsin', 'WI'], ['Wyoming', 'WY'] ] - end + def date_and_time(date_object, timezone = nil) + date_object = date_object.in_time_zone(timezone) if timezone + date_object.strftime('%m/%d/%Y %I:%M%P (%Z)') + end - # Append a parameter to a URL string - def url_with_param(param, val, url) - url + (url.include?('?') ? '&' : '?') + param + '=' + val - end + def us_states + [%w[Alabama AL], %w[Alaska AK], %w[Arizona AZ], %w[Arkansas AR], %w[California CA], %w[Colorado CO], %w[Connecticut CT], %w[Delaware DE], ['District of Columbia', 'DC'], %w[Florida FL], %w[Georgia GA], %w[Hawaii HI], %w[Idaho ID], %w[Illinois IL], %w[Indiana IN], %w[Iowa IA], %w[Kansas KS], %w[Kentucky KY], %w[Louisiana LA], %w[Maine ME], %w[Maryland MD], %w[Massachusetts MA], %w[Michigan MI], %w[Minnesota MN], %w[Mississippi MS], %w[Missouri MO], %w[Montana MT], %w[Nebraska NE], %w[Nevada NV], ['New Hampshire', 'NH'], ['New Jersey', 'NJ'], ['New Mexico', 'NM'], ['New York', 'NY'], ['North Carolina', 'NC'], ['North Dakota', 'ND'], %w[Ohio OH], %w[Oklahoma OK], %w[Oregon OR], %w[Pennsylvania PA], ['Puerto Rico', 'PR'], ['Rhode Island', 'RI'], ['South Carolina', 'SC'], ['South Dakota', 'SD'], %w[Tennessee TN], %w[Texas TX], %w[Utah UT], %w[Vermont VT], %w[Virginia VA], %w[Washington WA], ['West Virginia', 'WV'], %w[Wisconsin WI], %w[Wyoming WY]] + end - # Prepend 'http://' if it is not present in a given url - # Used for linking to nonprofit-provided website - def add_http url - if url[/^http:\/\//] || url[/^https:\/\//] - url - else - 'http://' + url - end - end + # Append a parameter to a URL string + def url_with_param(param, val, url) + url + (url.include?('?') ? '&' : '?') + param + '=' + val + end + # Prepend 'http://' if it is not present in a given url + # Used for linking to nonprofit-provided website + def add_http(url) + if url[%r{^http://}] || url[%r{^https://}] + url + else + 'http://' + url + end + end end diff --git a/app/helpers/card_helper.rb b/app/helpers/card_helper.rb index 7a9309e6..0dd63536 100644 --- a/app/helpers/card_helper.rb +++ b/app/helpers/card_helper.rb @@ -1,23 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CardHelper + def brand_file(brand) + if brand == 'Visa' || brand == 'visa' || brand == 'VISA' + 'visa' + elsif brand == 'American Express' || brand == 'amex' + 'amex' + elsif brand == 'Discover' || brand == 'Discover Card' || brand == 'discover' + 'discover' + elsif brand == 'MasterCard' || brand == 'Mastercard' || brand == 'mastercard' + 'mastercard' + end + end - def brand_file(brand) - if brand == 'Visa' || brand == 'visa' || brand == 'VISA' - 'visa' - elsif brand == 'American Express' || brand == 'amex' - 'amex' - elsif brand == 'Discover' || brand == 'Discover Card' || brand == 'discover' - 'discover' - elsif brand == 'MasterCard' || brand == 'Mastercard' || brand == 'mastercard' - 'mastercard' - end - end + def current_card + current_user&.profile&.card + end - def current_card - current_user && current_user.profile.card - end - - def expiration_years - (0..15).map{|n| (Date.today + n.years).year} - end + def expiration_years + (0..15).map { |n| (Date.today + n.years).year } + end end diff --git a/app/helpers/devise_helper.rb b/app/helpers/devise_helper.rb index c36671b5..03359923 100644 --- a/app/helpers/devise_helper.rb +++ b/app/helpers/devise_helper.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module DeviseHelper def devise_error_messages! if resource && !resource.errors.empty? - resource.errors.first.first.to_s + ' ' + + resource.errors.first.first.to_s + ' ' + resource.errors.first.second end end diff --git a/app/helpers/nonprofits_helper.rb b/app/helpers/nonprofits_helper.rb index 5d9ea441..c721b02f 100644 --- a/app/helpers/nonprofits_helper.rb +++ b/app/helpers/nonprofits_helper.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module NonprofitsHelper - - def managed_npo_card_json - if current_user - if params[:nonprofit_id] && current_role?(:super_admin) - raw(Nonprofit.find(params[:nonprofit_id]).active_card.to_json) - elsif administered_nonprofit && administered_nonprofit.active_card - raw(administered_nonprofit.active_card.to_json) - end - else - 'undefined' - end - end - + def managed_npo_card_json + if current_user + if params[:nonprofit_id] && current_role?(:super_admin) + raw(Nonprofit.find(params[:nonprofit_id]).active_card.to_json) + elsif administered_nonprofit&.active_card + raw(administered_nonprofit.active_card.to_json) + end + else + 'undefined' + end + end end diff --git a/app/helpers/onboard_helper.rb b/app/helpers/onboard_helper.rb index 1cb832a5..ebc34eda 100644 --- a/app/helpers/onboard_helper.rb +++ b/app/helpers/onboard_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module OnboardHelper end diff --git a/app/helpers/pricing_helper.rb b/app/helpers/pricing_helper.rb index db1d7b48..e6022535 100644 --- a/app/helpers/pricing_helper.rb +++ b/app/helpers/pricing_helper.rb @@ -1,8 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module PricingHelper -private - def nonprofit_email - return nil if @nonprofit.nil? - @nonprofit.email || GetData.chain(@nonprofit.users.first, :email) - end + private + + def nonprofit_email + return nil if @nonprofit.nil? + + @nonprofit.email || GetData.chain(@nonprofit.users.first, :email) + end end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb index e40303e4..dfca3351 100755 --- a/app/helpers/profiles_helper.rb +++ b/app/helpers/profiles_helper.rb @@ -1,12 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ProfilesHelper - - def get_shortened_name name + def get_shortened_name(name) if name name.length > 18 ? name[0..18] + '...' : name else 'Your Account' end end - end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb index cd5fb472..d4046c71 100644 --- a/app/jobs/application_job.rb +++ b/app/jobs/application_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ApplicationJob < ActiveJob::Base diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 11e5250b..1699f1b3 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AdminMailer < BaseMailer - # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 95021647..605976a3 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BaseMailer < ActionMailer::Base include Roadie::Rails::Automatic include Devise::Controllers::UrlHelpers add_template_helper(ApplicationHelper) - default :from => Settings.mailer.default_from + default from: Settings.mailer.default_from layout 'email' end diff --git a/app/mailers/billing_subscription_mailer.rb b/app/mailers/billing_subscription_mailer.rb index 93240743..971f4dbd 100644 --- a/app/mailers/billing_subscription_mailer.rb +++ b/app/mailers/billing_subscription_mailer.rb @@ -1,13 +1,13 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingSubscriptionMailer < BaseMailer - def failed_notice(np_id) @nonprofit = Nonprofit.find(np_id) @billing_subscription = @nonprofit.billing_subscription @card = @nonprofit.active_card @billing_plan = @billing_subscription.billing_plan - @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) - mail(to: @emails, subject: "Action Needed, Please Update Your #{Settings.general.name} Account") + @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) + mail(to: @emails, subject: "Action Needed, Please Update Your #{Settings.general.name} Account") end - end diff --git a/app/mailers/campaign_mailer.rb b/app/mailers/campaign_mailer.rb index 6919bef2..72fcb0ae 100644 --- a/app/mailers/campaign_mailer.rb +++ b/app/mailers/campaign_mailer.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignMailer < BaseMailer + def creation_followup(campaign) + @creator_profile = campaign.profile + @campaign = campaign + mail(to: @creator_profile.user.email, subject: "Get your new campaign rolling! (via #{Settings.general.name})") + end - def creation_followup(campaign) - @creator_profile = campaign.profile - @campaign = campaign - mail(:to => @creator_profile.user.email, :subject => "Get your new campaign rolling! (via #{Settings.general.name})") - end - - def federated_creation_followup(campaign) - @creator_profile = campaign.profile - @campaign = campaign - mail(:to => @creator_profile.user.email, :subject => "Get your new campaign rolling! (via #{Settings.general.name})") - end + def federated_creation_followup(campaign) + @creator_profile = campaign.profile + @campaign = campaign + mail(to: @creator_profile.user.email, subject: "Get your new campaign rolling! (via #{Settings.general.name})") + end end diff --git a/app/mailers/donation_mailer.rb b/app/mailers/donation_mailer.rb index 35daa7f2..d86a54bf 100644 --- a/app/mailers/donation_mailer.rb +++ b/app/mailers/donation_mailer.rb @@ -1,36 +1,38 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class DonationMailer < BaseMailer - - # Used for both one-time and recurring donations + # Used for both one-time and recurring donations # can pass in array of admin user_ids to send to only some -- if falsey/empty, will send to all - def donor_payment_notification(donation_id, locale=I18n.locale) - @donation = Donation.find(donation_id) - @nonprofit = @donation.nonprofit - if @donation.campaign && ActionView::Base.full_sanitizer.sanitize(@donation.campaign.receipt_message).present? + def donor_payment_notification(donation_id, locale = I18n.locale) + @donation = Donation.find(donation_id) + @nonprofit = @donation.nonprofit + if @donation.campaign && ActionView::Base.full_sanitizer.sanitize(@donation.campaign.receipt_message).present? @thank_you_note = @donation.campaign.receipt_message else - @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, {'NAME' => @donation.supporter.name}) + @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, 'NAME' => @donation.supporter.name) end @charge = @donation.charges.last - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email from = Format::Name.email_from_np(@nonprofit.name) I18n.with_locale(locale) do - mail( - to: @donation.supporter.email, - from: from, - reply_to: reply_to, - subject: I18n.t('mailer.donations.donor_direct_debit_notification.subject', nonprofit_name: @nonprofit.name)) + mail( + to: @donation.supporter.email, + from: from, + reply_to: reply_to, + subject: I18n.t('mailer.donations.donor_direct_debit_notification.subject', nonprofit_name: @nonprofit.name) + ) end end - def donor_direct_debit_notification(donation_id, locale=I18n.locale) + def donor_direct_debit_notification(donation_id, locale = I18n.locale) @donation = Donation.find(donation_id) @nonprofit = @donation.nonprofit - if @donation.campaign && ActionView::Base.full_sanitizer.sanitize(@donation.campaign.receipt_message).present? + if @donation.campaign && ActionView::Base.full_sanitizer.sanitize(@donation.campaign.receipt_message).present? @thank_you_note = @donation.campaign.receipt_message else - @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, {'NAME' => @donation.supporter.name}) + @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, 'NAME' => @donation.supporter.name) end reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email @@ -45,87 +47,86 @@ class DonationMailer < BaseMailer end end - # Used for both one-time and recurring donations - def nonprofit_payment_notification(donation_id, user_id=nil) - @donation = Donation.find(donation_id) - @charge = @donation.charges.last - @nonprofit = @donation.nonprofit + # Used for both one-time and recurring donations + def nonprofit_payment_notification(donation_id, user_id = nil) + @donation = Donation.find(donation_id) + @charge = @donation.charges.last + @nonprofit = @donation.nonprofit @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, @donation.campaign ? 'notify_campaigns' : 'notify_payments') if user_id em = User.find(user_id).email # return unless @emails.include?(em) @emails = [em] end - mail(to: @emails, subject: "Donation receipt for #{@donation.supporter.name}") - end + mail(to: @emails, subject: "Donation receipt for #{@donation.supporter.name}") + end def nonprofit_failed_recurring_donation(donation_id) - @donation = Donation.find(donation_id) - @nonprofit = @donation.nonprofit - @charge = @donation.charges.last + @donation = Donation.find(donation_id) + @nonprofit = @donation.nonprofit + @charge = @donation.charges.last @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, @donation.campaign ? 'notify_campaigns' : 'notify_payments') - mail(to: @emails, subject: "Recurring donation payment failure for #{@donation.supporter.name || @donation.supporter.email}") + mail(to: @emails, subject: "Recurring donation payment failure for #{@donation.supporter.name || @donation.supporter.email}") end def donor_failed_recurring_donation(donation_id) - @donation = Donation.find(donation_id) - @nonprofit = @donation.nonprofit - @charge = @donation.charges.last - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + @donation = Donation.find(donation_id) + @nonprofit = @donation.nonprofit + @charge = @donation.charges.last + reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email from = Format::Name.email_from_np(@nonprofit.name) - mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Donation payment failure for #{@nonprofit.name}") + mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Donation payment failure for #{@nonprofit.name}") end def nonprofit_recurring_donation_cancellation(donation_id) - @donation = Donation.find(donation_id) - @nonprofit = @donation.nonprofit - @charge = @donation.charges.last + @donation = Donation.find(donation_id) + @nonprofit = @donation.nonprofit + @charge = @donation.charges.last @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, @donation.campaign ? 'notify_campaigns' : 'notify_payments') - mail(to: @emails, subject: "Recurring donation cancelled for #{@donation.supporter.name || @donation.supporter.email}") - end + mail(to: @emails, subject: "Recurring donation cancelled for #{@donation.supporter.name || @donation.supporter.email}") + end - def nonprofit_recurring_donation_change_amount(donation_id, previous_amount=nil) - @donation = RecurringDonation.find(donation_id).donation - @nonprofit = @donation.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') - @previous_amount = previous_amount - mail(to: @emails, subject:"Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") - end + def nonprofit_recurring_donation_change_amount(donation_id, previous_amount = nil) + @donation = RecurringDonation.find(donation_id).donation + @nonprofit = @donation.nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') + @previous_amount = previous_amount + mail(to: @emails, subject: "Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") + end - def donor_recurring_donation_change_amount(donation_id, previous_amount=nil) - @donation = RecurringDonation.find(donation_id).donation - @nonprofit = @donation.nonprofit - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email - if @nonprofit.miscellaneous_np_info && ActionView::Base.full_sanitizer.sanitize(@nonprofit.miscellaneous_np_info.change_amount_message).present? - @thank_you_note = @nonprofit.miscellaneous_np_info.change_amount_message - else - @thank_you_note = nil - end - from = Format::Name.email_from_np(@nonprofit.name) - @previous_amount = previous_amount - mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Recurring donation amount changed for #{@nonprofit.name}") - end + def donor_recurring_donation_change_amount(donation_id, previous_amount = nil) + @donation = RecurringDonation.find(donation_id).donation + @nonprofit = @donation.nonprofit + reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + if @nonprofit.miscellaneous_np_info && ActionView::Base.full_sanitizer.sanitize(@nonprofit.miscellaneous_np_info.change_amount_message).present? + @thank_you_note = @nonprofit.miscellaneous_np_info.change_amount_message + else + @thank_you_note = nil + end + from = Format::Name.email_from_np(@nonprofit.name) + @previous_amount = previous_amount + mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Recurring donation amount changed for #{@nonprofit.name}") + end - def nonprofit_recurring_donation_change_amount(donation_id, previous_amount=nil) - @donation = RecurringDonation.find(donation_id).donation - @nonprofit = @donation.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') - @previous_amount = previous_amount - mail(to: @emails, subject:"Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") - end - - def donor_recurring_donation_change_amount(donation_id, previous_amount=nil) - @donation = RecurringDonation.find(donation_id).donation - @nonprofit = @donation.nonprofit - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email - if @nonprofit.miscellaneous_np_info && ActionView::Base.full_sanitizer.sanitize(@nonprofit.miscellaneous_np_info.change_amount_message).present? - @thank_you_note = @nonprofit.miscellaneous_np_info.change_amount_message - else - @thank_you_note = nil - end - from = Format::Name.email_from_np(@nonprofit.name) - @previous_amount = previous_amount - mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Recurring donation amount changed for #{@nonprofit.name}") - end + def nonprofit_recurring_donation_change_amount(donation_id, previous_amount = nil) + @donation = RecurringDonation.find(donation_id).donation + @nonprofit = @donation.nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') + @previous_amount = previous_amount + mail(to: @emails, subject: "Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") + end + def donor_recurring_donation_change_amount(donation_id, previous_amount = nil) + @donation = RecurringDonation.find(donation_id).donation + @nonprofit = @donation.nonprofit + reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + if @nonprofit.miscellaneous_np_info && ActionView::Base.full_sanitizer.sanitize(@nonprofit.miscellaneous_np_info.change_amount_message).present? + @thank_you_note = @nonprofit.miscellaneous_np_info.change_amount_message + else + @thank_you_note = nil + end + from = Format::Name.email_from_np(@nonprofit.name) + @previous_amount = previous_amount + mail(to: @donation.supporter.email, from: from, reply_to: reply_to, subject: "Recurring donation amount changed for #{@nonprofit.name}") + end end diff --git a/app/mailers/event_mailer.rb b/app/mailers/event_mailer.rb index 49a32b63..47f5ba2f 100644 --- a/app/mailers/event_mailer.rb +++ b/app/mailers/event_mailer.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventMailer < BaseMailer + helper :application - helper :application - - include Devise::Controllers::UrlHelpers - - def creation_followup(event) - @creator_profile = event.profile - @event = event - mail(:to => @creator_profile.user.email, :subject => "Get your new event rolling on #{Settings.general.name}!") - end + include Devise::Controllers::UrlHelpers + def creation_followup(event) + @creator_profile = event.profile + @event = event + mail(to: @creator_profile.user.email, subject: "Get your new event rolling on #{Settings.general.name}!") + end end diff --git a/app/mailers/export_mailer.rb b/app/mailers/export_mailer.rb index 2df3d36f..477eabf1 100644 --- a/app/mailers/export_mailer.rb +++ b/app/mailers/export_mailer.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ExportMailer < BaseMailer - # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # @@ -18,7 +19,6 @@ class ExportMailer < BaseMailer mail(to: @export.user.email, subject: 'Your payment export has failed') end - def export_recurring_donations_completed_notification(export) @export = export diff --git a/app/mailers/generic_mailer.rb b/app/mailers/generic_mailer.rb index 966c5f9e..c540e55a 100644 --- a/app/mailers/generic_mailer.rb +++ b/app/mailers/generic_mailer.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class GenericMailer < BaseMailer - - def generic_mail(from_email, from_name, message, subject, to_email, to_name) + def generic_mail(from_email, from_name, message, subject, to_email, _to_name) @from_email = from_email @from_name = from_name @message = message - mail(:to => to_email, :from => "#{from_name} <#{Settings.mailer.email}>", :reply_to => from_email, :subject => "#{subject}") + mail(to: to_email, from: "#{from_name} <#{Settings.mailer.email}>", reply_to: from_email, subject: subject.to_s) end # For sending a system notice to super admins @@ -16,5 +17,4 @@ class GenericMailer < BaseMailer emails = QueryUsers.super_admin_emails mail(to: emails, from: "#{@from_name} <#{@from_email}>", reply_to: @from_email, subject: options[:subject], template_name: 'generic_mail') end - end diff --git a/app/mailers/import_mailer.rb b/app/mailers/import_mailer.rb index 92a126f1..3adb30d0 100644 --- a/app/mailers/import_mailer.rb +++ b/app/mailers/import_mailer.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ImportMailer < BaseMailer - - def import_completed_notification(import_id) - @import = Import.find(import_id) - @nonprofit = @import.nonprofit - mail(to: @import.user.email, subject: "Your import is complete!") - end - + def import_completed_notification(import_id) + @import = Import.find(import_id) + @nonprofit = @import.nonprofit + mail(to: @import.user.email, subject: 'Your import is complete!') + end end diff --git a/app/mailers/nonprofit_admin_mailer.rb b/app/mailers/nonprofit_admin_mailer.rb index f7cad1bb..b0198d38 100644 --- a/app/mailers/nonprofit_admin_mailer.rb +++ b/app/mailers/nonprofit_admin_mailer.rb @@ -1,28 +1,28 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitAdminMailer < BaseMailer + def new_invite(role, raw_token) + @user = role.user + @title_with_article = Format::Indefinitize.with_article(role.name.to_s.titleize) + @nonprofit = role.host + @token = raw_token + mail(to: @user.email, subject: "You're now #{@title_with_article} of #{@nonprofit.name} on #{Settings.general.name}. Let's set your password.") + end - def new_invite(role, raw_token) - @user = role.user - @title_with_article = Format::Indefinitize.with_article(role.name.to_s.titleize) - @nonprofit = role.host - @token = raw_token - mail(:to => @user.email, :subject => "You're now #{@title_with_article} of #{@nonprofit.name} on #{Settings.general.name}. Let's set your password.") - end + def existing_invite(role) + @user = role.user + @title_with_article = Format::Indefinitize.with_article(role.name.to_s.titleize) + @nonprofit = role.host + mail(to: @user.email, subject: "You're now #{@title_with_article} of #{@nonprofit.name} on #{Settings.general.name}.") + end - def existing_invite(role) - @user = role.user - @title_with_article = Format::Indefinitize.with_article(role.name.to_s.titleize) - @nonprofit = role.host - mail(:to => @user.email, :subject => "You're now #{@title_with_article} of #{@nonprofit.name} on #{Settings.general.name}.") - end - - def supporter_fundraiser(event_or_campaign) - @fundraiser = event_or_campaign - @kind = event_or_campaign.class.name.downcase || 'event' - @nonprofit = event_or_campaign.nonprofit - @profile = event_or_campaign.profile - recipients = @nonprofit.nonprofit_personnel_emails - mail(to: recipients, subject: "A Supporter has created #{Format::Indefinitize.with_article(@kind.capitalize)} for your Nonprofit!") - end + def supporter_fundraiser(event_or_campaign) + @fundraiser = event_or_campaign + @kind = event_or_campaign.class.name.downcase || 'event' + @nonprofit = event_or_campaign.nonprofit + @profile = event_or_campaign.profile + recipients = @nonprofit.nonprofit_personnel_emails + mail(to: recipients, subject: "A Supporter has created #{Format::Indefinitize.with_article(@kind.capitalize)} for your Nonprofit!") + end end - diff --git a/app/mailers/nonprofit_mailer.rb b/app/mailers/nonprofit_mailer.rb index e57bd62d..445c7b9e 100644 --- a/app/mailers/nonprofit_mailer.rb +++ b/app/mailers/nonprofit_mailer.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitMailer < BaseMailer - def failed_verification_notice(np) @nonprofit = np @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') @@ -13,92 +14,93 @@ class NonprofitMailer < BaseMailer mail(to: @emails, subject: "Verification successful on #{Settings.general.name}!") end - def refund_notification(refund_id) - @refund = Refund.find(refund_id) - @charge = @refund.charge - @nonprofit = @refund.payment.nonprofit + def refund_notification(refund_id) + @refund = Refund.find(refund_id) + @charge = @refund.charge + @nonprofit = @refund.payment.nonprofit @supporter = @refund.payment.supporter - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payments') - mail(to: @emails, subject: "A new refund has been made for $#{Format::Currency.cents_to_dollars(@refund.amount)}") - end + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payments') + mail(to: @emails, subject: "A new refund has been made for $#{Format::Currency.cents_to_dollars(@refund.amount)}") + end - def new_bank_account_notification(ba) - @nonprofit = ba.nonprofit - @bank_account = ba - @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) - mail(to: @emails, subject: "We need to confirm the new bank account") - end + def new_bank_account_notification(ba) + @nonprofit = ba.nonprofit + @bank_account = ba + @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) + mail(to: @emails, subject: 'We need to confirm the new bank account') + end - def pending_payout_notification(payout_id) - @payout = Payout.find(payout_id) - @nonprofit = @payout.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') - mail(to: @emails, subject: "Payout of available balance now pending") - end + def pending_payout_notification(payout_id) + @payout = Payout.find(payout_id) + @nonprofit = @payout.nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + mail(to: @emails, subject: 'Payout of available balance now pending') + end - def successful_payout_notification(payout) - @nonprofit = payout.nonprofit - @payout = payout - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') - mail(to: @emails, subject: "Payout of available balance succeeded") - end + def successful_payout_notification(payout) + @nonprofit = payout.nonprofit + @payout = payout + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + mail(to: @emails, subject: 'Payout of available balance succeeded') + end - def failed_payout_notification(payout) - @nonprofit = payout.nonprofit - @payout = payout - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') - mail(to: @emails, subject: "Payout could not be completed") - end + def failed_payout_notification(payout) + @nonprofit = payout.nonprofit + @payout = payout + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + mail(to: @emails, subject: 'Payout could not be completed') + end - def failed_recurring_donation(recurring_donation) - @recurring_donation = recurring_donation - @nonprofit = recurring_donation.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') - mail(to: @emails, subject: "A recurring donation from one of your supporters had a payment failure.") - end + def failed_recurring_donation(recurring_donation) + @recurring_donation = recurring_donation + @nonprofit = recurring_donation.nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') + mail(to: @emails, subject: 'A recurring donation from one of your supporters had a payment failure.') + end - def cancelled_recurring_donation(recurring_donation) - @recurring_donation = recurring_donation - @nonprofit = recurring_donation.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') - mail(to: @emails, subject: "A recurring donation from one of your supporters was cancelled.") - end + def cancelled_recurring_donation(recurring_donation) + @recurring_donation = recurring_donation + @nonprofit = recurring_donation.nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_recurring_donations') + mail(to: @emails, subject: 'A recurring donation from one of your supporters was cancelled.') + end - def verified_notification(nonprofit) - @nonprofit = nonprofit - @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) - mail(to: @emails, subject: "Your nonprofit has been verified!") - end + def verified_notification(nonprofit) + @nonprofit = nonprofit + @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id) + mail(to: @emails, subject: 'Your nonprofit has been verified!') + end - def button_code(nonprofit, to_email, to_name, from_email, message, code) - @nonprofit = nonprofit - @to_email = to_email - @to_name = to_name - @from = from_email - @message = message - @code = code + def button_code(nonprofit, to_email, to_name, from_email, message, code) + @nonprofit = nonprofit + @to_email = to_email + @to_name = to_name + @from = from_email + @message = message + @code = code from = Format::Name.email_from_np(@nonprofit.name) - mail(to: to_email, from: from, reply_to: from_email, subject: "Please include this donate button code on the website") - end + mail(to: to_email, from: from, reply_to: from_email, subject: 'Please include this donate button code on the website') + end def invoice_payment_notification(nonprofit_id, payment) @nonprofit = Nonprofit.find(nonprofit_id) @payment = payment @emails = QueryUsers.all_nonprofit_user_emails(@nonprofit.id, [:nonprofit_admin]) @month_name = Date::MONTHNAMES[payment.date.month] - mail(to: @emails, subject: "#{Settings.general.name} Subscription Receipt for #{@month_name}") + mail(to: @emails, subject: "#{Settings.general.name} Subscription Receipt for #{@month_name}") end - # pass in all of: - # {is_unsubscribed_from_emails, supporter_email, message, email_unsubscribe_uuid, nonprofit_id, from_email, subject} - def supporter_message(args) - return if args[:is_unsubscribed_from_emails] || args[:supporter_email].blank? - @message = args[:message] - @uuid = args[:email_unsubscribe_uuid] - @nonprofit = Nonprofit.find args[:nonprofit_id] + # pass in all of: + # {is_unsubscribed_from_emails, supporter_email, message, email_unsubscribe_uuid, nonprofit_id, from_email, subject} + def supporter_message(args) + return if args[:is_unsubscribed_from_emails] || args[:supporter_email].blank? + + @message = args[:message] + @uuid = args[:email_unsubscribe_uuid] + @nonprofit = Nonprofit.find args[:nonprofit_id] from = Format::Name.email_from_np(@nonprofit.name) - mail(to: args[:supporter_email], reply_to: args[:from_email], from: from, subject: args[:subject]) - end + mail(to: args[:supporter_email], reply_to: args[:from_email], from: from, subject: args[:subject]) + end def setup_verification(np_id) @nonprofit = Nonprofit.find(np_id) @@ -113,6 +115,4 @@ class NonprofitMailer < BaseMailer @emails = QueryUsers.all_nonprofit_user_emails(np_id, [:nonprofit_admin]) mail(to: @emails, reply_to: 'support@commitchange.com', from: "#{Settings.general.name} Support", subject: "A hearty welcome from the #{Settings.general.name} team") end - end - diff --git a/app/mailers/payment_mailer.rb b/app/mailers/payment_mailer.rb index 28b7d52d..21762c5d 100644 --- a/app/mailers/payment_mailer.rb +++ b/app/mailers/payment_mailer.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class PaymentMailer < BaseMailer - # Send a donation receipt to a single admin # or a ticket receipt def resend_admin_receipt(payment_id, user_id) @@ -22,5 +23,4 @@ class PaymentMailer < BaseMailer return TicketMailer.followup(payment.tickets.pluck(:id), payment.charge.id).deliver end end - end diff --git a/app/mailers/recurring_donation_mailer.rb b/app/mailers/recurring_donation_mailer.rb index d1a4a649..ce64e58c 100644 --- a/app/mailers/recurring_donation_mailer.rb +++ b/app/mailers/recurring_donation_mailer.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RecurringDonationMailer < BaseMailer + def send_cancellation_notices(recurring_donation) + UserMailer.recurring_donation_cancelled(recurring_donation).deliver + NonprofitMailer.cancelled_recurring_donation(recurring_donation).deliver + recurring_donation + end - def send_cancellation_notices(recurring_donation) - UserMailer.recurring_donation_cancelled(recurring_donation).deliver - NonprofitMailer.cancelled_recurring_donation(recurring_donation).deliver - return recurring_donation - end - - def send_failure_notifications(recurring_donation) - UserMailer.recurring_donation_failure(recurring_donation).deliver - NonprofitMailer.failed_recurring_donation(recurring_donation).deliver - end + def send_failure_notifications(recurring_donation) + UserMailer.recurring_donation_failure(recurring_donation).deliver + NonprofitMailer.failed_recurring_donation(recurring_donation).deliver + end end diff --git a/app/mailers/testing.rb b/app/mailers/testing.rb index a82ab782..9d064245 100644 --- a/app/mailers/testing.rb +++ b/app/mailers/testing.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Testing < ActionMailer::Base - default from: "from@example.com" + default from: 'from@example.com' end diff --git a/app/mailers/ticket_mailer.rb b/app/mailers/ticket_mailer.rb index 623646d6..7f39b499 100644 --- a/app/mailers/ticket_mailer.rb +++ b/app/mailers/ticket_mailer.rb @@ -1,22 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketMailer < BaseMailer - helper :application # Pass in ticket_ids, event_id, and supporter - def followup(ticket_ids, charge_id=nil) + def followup(ticket_ids, charge_id = nil) @charge = charge_id ? Charge.find(charge_id) : nil - @tickets = Ticket.where("id IN(?)", ticket_ids) + @tickets = Ticket.where('id IN(?)', ticket_ids) @event = @tickets.last.event @supporter = @tickets.last.supporter @nonprofit = @supporter.nonprofit from = Format::Name.email_from_np(@nonprofit.name) reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email - mail(from: from, to: @supporter.email, reply_to: reply_to, subject: "Your tickets#{@charge ? ' and receipt ' : ' '}for: #{@event.name}") + mail(from: from, to: @supporter.email, reply_to: reply_to, subject: "Your tickets#{@charge ? ' and receipt ' : ' '}for: #{@event.name}") end - def receipt_admin(ticket_ids, user_id=nil) - @tickets = Ticket.where("id IN (?)", ticket_ids) + def receipt_admin(ticket_ids, user_id = nil) + @tickets = Ticket.where('id IN (?)', ticket_ids) @charge = @tickets.last.charge @supporter = @tickets.last.supporter @event = @tickets.last.event @@ -25,9 +26,9 @@ class TicketMailer < BaseMailer if user_id em = User.find(user_id).email return unless recipients.include?(em) + recipients = [em] end mail(to: recipients, subject: "Ticket redeemed for #{@event.name} - #{@supporter.name}") end - end diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb index f216cc0e..b9a01478 100644 --- a/app/mailers/user_mailer.rb +++ b/app/mailers/user_mailer.rb @@ -1,26 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class UserMailer < BaseMailer - - def refund_receipt(refund_id) - @refund = Refund.find(refund_id) + def refund_receipt(refund_id) + @refund = Refund.find(refund_id) @nonprofit = @refund.payment.nonprofit - @charge = @refund.charge + @charge = @refund.charge @supporter = @refund.payment.supporter - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email from = Format::Name.email_from_np(@nonprofit.name) - mail(to: @supporter.email, from: from, reply_to: reply_to, subject: "Your refund receipt for #{@nonprofit.name}") - end + mail(to: @supporter.email, from: from, reply_to: reply_to, subject: "Your refund receipt for #{@nonprofit.name}") + end - def recurring_donation_failure(recurring_donation) - @recurring_donation = recurring_donation - mail(:to => @recurring_donation.email, - :subject => ("We couldn't process your recurring donation towards #{@recurring_donation.nonprofit.name}.")) - end - - def recurring_donation_cancelled(recurring_donation) - @recurring_donation = recurring_donation - mail(:to => @recurring_donation.email, - :subject => ("Your recurring donation towards #{@recurring_donation.nonprofit.name} was successfully cancelled.")) - end + def recurring_donation_failure(recurring_donation) + @recurring_donation = recurring_donation + mail(to: @recurring_donation.email, + subject: "We couldn't process your recurring donation towards #{@recurring_donation.nonprofit.name}.") + end + def recurring_donation_cancelled(recurring_donation) + @recurring_donation = recurring_donation + mail(to: @recurring_donation.email, + subject: "Your recurring donation towards #{@recurring_donation.nonprofit.name} was successfully cancelled.") + end end diff --git a/app/models/activity.rb b/app/models/activity.rb index 20a763bf..8dd456c0 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,5 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Activity < ApplicationRecord - end - diff --git a/app/models/application_record.rb b/app/models/application_record.rb index 52fec221..9f8f3f0d 100644 --- a/app/models/application_record.rb +++ b/app/models/application_record.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true -end \ No newline at end of file + self.abstract_class = true +end diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb index b7eedb59..aa3d7f98 100644 --- a/app/models/bank_account.rb +++ b/app/models/bank_account.rb @@ -1,42 +1,41 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BankAccount < ApplicationRecord + # TODO + # attr_accessible \ + # :name, # str (readable bank name identifier, eg. "Wells Fargo *1234") + # :confirmation_token, # str (randomly generated private token for email confirmation) + # :account_number, # str (last digits only) + # :bank_name, # str + # :pending_verification, # bool (whether this bank account is still awaiting email confirmation) + # :status, # str + # :email, # str (contact email associated with the user who created this bank account) + # :deleted, # bool (soft delete flag) + # :stripe_bank_account_token, # str + # :stripe_bank_account_id, # str + # :nonprofit_id, :nonprofit - #TODO - # attr_accessible \ - # :name, # str (readable bank name identifier, eg. "Wells Fargo *1234") - # :confirmation_token, # str (randomly generated private token for email confirmation) - # :account_number, # str (last digits only) - # :bank_name, # str - # :pending_verification, # bool (whether this bank account is still awaiting email confirmation) - # :status, # str - # :email, # str (contact email associated with the user who created this bank account) - # :deleted, # bool (soft delete flag) - # :stripe_bank_account_token, # str - # :stripe_bank_account_id, # str - # :nonprofit_id, :nonprofit + # validates :stripe_bank_account_token, presence: true, uniqueness: true + # validates :stripe_bank_account_id, presence: true, uniqueness: true + # validates :nonprofit, presence: true + # validates :email, presence: true, format: {with: Email::Regex} + # validate :nonprofit_must_be_vetted, on: :create + # validate :nonprofit_has_stripe_account - #validates :stripe_bank_account_token, presence: true, uniqueness: true - # validates :stripe_bank_account_id, presence: true, uniqueness: true - #validates :nonprofit, presence: true - #validates :email, presence: true, format: {with: Email::Regex} - #validate :nonprofit_must_be_vetted, on: :create - #validate :nonprofit_has_stripe_account + has_many :payouts + belongs_to :nonprofit - has_many :payouts - belongs_to :nonprofit + def nonprofit_must_be_vetted + errors.add(:nonprofit, 'must be vetted') unless nonprofit&.vetted + end - def nonprofit_must_be_vetted - errors.add(:nonprofit, "must be vetted") unless self.nonprofit && self.nonprofit.vetted - end - - - def nonprofit_has_stripe_account - errors.add(:nonprofit, 'must have a Stripe account id') if !self.nonprofit || self.nonprofit.stripe_account_id.blank? - end - - # Manually cause an instance to become invalid - def invalidate! - @not_valid = true - end + def nonprofit_has_stripe_account + errors.add(:nonprofit, 'must have a Stripe account id') if !nonprofit || nonprofit.stripe_account_id.blank? + end + # Manually cause an instance to become invalid + def invalidate! + @not_valid = true + end end diff --git a/app/models/billing_plan.rb b/app/models/billing_plan.rb index b5468646..ce5eec79 100644 --- a/app/models/billing_plan.rb +++ b/app/models/billing_plan.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingPlan < ApplicationRecord - Names = ['Starter', 'Fundraising', 'Supporter Management'] - DefaultAmounts = [0, 9900, 29900] # in pennies + Names = ['Starter', 'Fundraising', 'Supporter Management'].freeze + DefaultAmounts = [0, 9900, 29_900].freeze # in pennies - #TODO - # attr_accessible \ - # :name, #str: readable name - # :tier, #int: 0-4 (0: Free, 1: Fundraising, 2: Supporter Management) - # :amount, #int (cents) - # :stripe_plan_id, #str (matches plan ID in Stripe) Not needed if it's not a paying subscription - # :interval, #str ('monthly', 'annual') - # :percentage_fee # 0.038 + # TODO + # attr_accessible \ + # :name, #str: readable name + # :tier, #int: 0-4 (0: Free, 1: Fundraising, 2: Supporter Management) + # :amount, #int (cents) + # :stripe_plan_id, #str (matches plan ID in Stripe) Not needed if it's not a paying subscription + # :interval, #str ('monthly', 'annual') + # :percentage_fee # 0.038 - has_many :billing_subscriptions + has_many :billing_subscriptions - validates :name, :presence => true - validates :amount, :presence => true + validates :name, presence: true + validates :amount, presence: true end diff --git a/app/models/billing_subscription.rb b/app/models/billing_subscription.rb index 85b5679e..66b08ede 100644 --- a/app/models/billing_subscription.rb +++ b/app/models/billing_subscription.rb @@ -1,32 +1,31 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingSubscription < ApplicationRecord + # TODO + # attr_accessible \ + # :nonprofit_id, :nonprofit, + # :billing_plan_id, :billing_plan, + # :stripe_subscription_id, + # :status # trialing, active, past_due, canceled, or unpaid - #TODO - # attr_accessible \ - # :nonprofit_id, :nonprofit, - # :billing_plan_id, :billing_plan, - # :stripe_subscription_id, - # :status # trialing, active, past_due, canceled, or unpaid + attr_accessor :stripe_plan_id, :manual + belongs_to :nonprofit + belongs_to :billing_plan - attr_accessor :stripe_plan_id, :manual - belongs_to :nonprofit - belongs_to :billing_plan + validates :nonprofit, presence: true + validates :billing_plan, presence: true - validates :nonprofit, presence: true - validates :billing_plan, presence: true - - def as_json(options={}) - h = super(options) - h[:plan_name] = self.billing_plan.name - h[:plan_amount] = self.billing_plan.amount / 100 - h - end - - def self.create_with_stripe(np, params) - bp = BillingPlan.find_by_stripe_plan_id params[:stripe_plan_id] - h = ConstructBillingSubscription.with_stripe np, bp - return np.create_billing_subscription h - end + def as_json(options = {}) + h = super(options) + h[:plan_name] = billing_plan.name + h[:plan_amount] = billing_plan.amount / 100 + h + end + def self.create_with_stripe(np, params) + bp = BillingPlan.find_by_stripe_plan_id params[:stripe_plan_id] + h = ConstructBillingSubscription.with_stripe np, bp + np.create_billing_subscription h + end end - diff --git a/app/models/campaign.rb b/app/models/campaign.rb index 137381c1..d9be17c9 100644 --- a/app/models/campaign.rb +++ b/app/models/campaign.rb @@ -1,186 +1,185 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Campaign < ApplicationRecord + # TODO + # attr_accessible \ + # :name, + # :tagline, + # :slug, # str: url name + # :total_supporters, + # :goal_amount, + # :nonprofit_id, + # :profile_id, + # :main_image, + # :remove_main_image, # for carrierwave + # :background_image, + # :remove_background_image, #bool carrierwave + # :banner_image, + # :remove_banner_image, + # :published, + # :video_url, #str + # :vimeo_video_id, + # :youtube_video_id, + # :summary, + # :recurring_fund, # bool: whether this is a recurring campaign + # :body, + # :goal_amount_dollars, #accessor: translated into goal_amount (cents) + # :show_total_raised, # bool + # :show_total_count, # bool + # :hide_activity_feed, # bool + # :end_datetime, + # :deleted, #bool (soft delete) + # :hide_goal, # bool + # :hide_thermometer, #bool + # :hide_title, # bool + # :receipt_message, # text + # :hide_custom_amounts, # boolean + # :parent_campaign_id, + # :reason_for_supporting, + # :default_reason_for_supporting - #TODO - # attr_accessible \ - # :name, - # :tagline, - # :slug, # str: url name - # :total_supporters, - # :goal_amount, - # :nonprofit_id, - # :profile_id, - # :main_image, - # :remove_main_image, # for carrierwave - # :background_image, - # :remove_background_image, #bool carrierwave - # :banner_image, - # :remove_banner_image, - # :published, - # :video_url, #str - # :vimeo_video_id, - # :youtube_video_id, - # :summary, - # :recurring_fund, # bool: whether this is a recurring campaign - # :body, - # :goal_amount_dollars, #accessor: translated into goal_amount (cents) - # :show_total_raised, # bool - # :show_total_count, # bool - # :hide_activity_feed, # bool - # :end_datetime, - # :deleted, #bool (soft delete) - # :hide_goal, # bool - # :hide_thermometer, #bool - # :hide_title, # bool - # :receipt_message, # text - # :hide_custom_amounts, # boolean - # :parent_campaign_id, - # :reason_for_supporting, - # :default_reason_for_supporting - - validate :end_datetime_cannot_be_in_past, :on => :create - validates :profile, :presence => true - validates :nonprofit, :presence => true - validates :goal_amount, - :presence => true, - :numericality => {:only_integer => true, :greater_than => 99} - validates :name, - :presence => true, - :length => {:maximum => 60} - validates :slug, uniqueness: {scope: :nonprofit_id, message: 'You already have a campaign with that URL.'}, presence: true + validate :end_datetime_cannot_be_in_past, on: :create + validates :profile, presence: true + validates :nonprofit, presence: true + validates :goal_amount, + presence: true, + numericality: { only_integer: true, greater_than: 99 } + validates :name, + presence: true, + length: { maximum: 60 } + validates :slug, uniqueness: { scope: :nonprofit_id, message: 'You already have a campaign with that URL.' }, presence: true attr_accessor :goal_amount_dollars - mount_uploader :main_image, CampaignMainImageUploader - mount_uploader :background_image, CampaignBackgroundImageUploader - mount_uploader :banner_image, CampaignBannerImageUploader + mount_uploader :main_image, CampaignMainImageUploader + mount_uploader :background_image, CampaignBackgroundImageUploader + mount_uploader :banner_image, CampaignBannerImageUploader - has_many :donations - has_many :charges, through: :donations - has_many :payments, { through: :donations, source: "payment" } - has_many :campaign_gift_options - has_many :campaign_gifts, through: :campaign_gift_options - has_many :supporters, :through => :donations - has_many :recurring_donations - has_many :roles, as: :host, dependent: :destroy - has_many :comments, as: :host, dependent: :destroy - has_many :activities, as: :host, dependent: :destroy - belongs_to :profile - belongs_to :nonprofit + has_many :donations + has_many :charges, through: :donations + has_many :payments, through: :donations, source: 'payment' + has_many :campaign_gift_options + has_many :campaign_gifts, through: :campaign_gift_options + has_many :supporters, through: :donations + has_many :recurring_donations + has_many :roles, as: :host, dependent: :destroy + has_many :comments, as: :host, dependent: :destroy + has_many :activities, as: :host, dependent: :destroy + belongs_to :profile + belongs_to :nonprofit belongs_to :parent_campaign, class_name: 'Campaign' has_many :children_campaigns, class_name: 'Campaign', foreign_key: 'parent_campaign_id' - scope :published, -> {where(:published => true)} - scope :active, -> {where(:published => true).where("end_datetime IS NULL OR end_datetime >= ?", Date.today)} - scope :past, -> {where(:published => true).where("end_datetime < ?", Date.today)} - scope :unpublished, -> {where(:published => [nil, false])} - scope :not_deleted, -> {where(deleted: [nil, false])} - scope :deleted, -> {where(deleted: true)} - scope :not_a_child, -> {where(parent_campaign_id: nil)} + scope :published, -> { where(published: true) } + scope :active, -> { where(published: true).where('end_datetime IS NULL OR end_datetime >= ?', Date.today) } + scope :past, -> { where(published: true).where('end_datetime < ?', Date.today) } + scope :unpublished, -> { where(published: [nil, false]) } + scope :not_deleted, -> { where(deleted: [nil, false]) } + scope :deleted, -> { where(deleted: true) } + scope :not_a_child, -> { where(parent_campaign_id: nil) } - before_validation do - if self.goal_amount_dollars.present? - self.goal_amount = (self.goal_amount_dollars.gsub(',','').to_f * 100).to_i - end - self - end + before_validation do + if goal_amount_dollars.present? + self.goal_amount = (goal_amount_dollars.delete(',').to_f * 100).to_i + end + self + end - before_validation(on: :create) do - unless self.slug - self.slug = Format::Url.convert_to_slug(name) - end - self.set_defaults - self - end + before_validation(on: :create) do + self.slug = Format::Url.convert_to_slug(name) unless slug + set_defaults + self + end - before_save do - self.parse_video_id if self.video_url && self.video_url_changed? - self - end + before_save do + parse_video_id if video_url && video_url_changed? + self + end - after_create do - user = self.profile.user - Role.create(name: :campaign_editor, user_id: user.id, host: self) - if child_campaign? - CampaignMailer.delay.federated_creation_followup(self) - else - CampaignMailer.delay.creation_followup(self) - end + after_create do + user = profile.user + Role.create(name: :campaign_editor, user_id: user.id, host: self) + if child_campaign? + CampaignMailer.delay.federated_creation_followup(self) + else + CampaignMailer.delay.creation_followup(self) + end - NonprofitAdminMailer.delay.supporter_fundraiser(self) unless QueryRoles.is_nonprofit_user?(user.id, self.nonprofit_id) - self - end + NonprofitAdminMailer.delay.supporter_fundraiser(self) unless QueryRoles.is_nonprofit_user?(user.id, nonprofit_id) + self + end - def set_defaults + def set_defaults + self.total_supporters = 1 + self.published = false if published.nil? + end - self.total_supporters = 1 - self.published = false if self.published.nil? - end + def parse_video_id + if video_url.include? 'vimeo' + self.vimeo_video_id = video_url.split('/').last + self.youtube_video_id = nil + elsif video_url.include? 'youtube' + match = video_url.match(/\?v=(.+)/) + return if match.nil? + self.youtube_video_id = match[1].split('&').first + self.vimeo_video_id = nil + elsif video_url.include? 'youtu.be' + self.youtube_video_id = video_url.split('/').last + self.vimeo_video_id = nil + elsif video_url.blank? + self.vimeo_video_id = nil + self.youtube_video_id = nil + end + self + end - def parse_video_id - if self.video_url.include? 'vimeo' - self.vimeo_video_id = self.video_url.split('/').last - self.youtube_video_id = nil - elsif self.video_url.include? 'youtube' - match = self.video_url.match(/\?v=(.+)/) - return if match.nil? - self.youtube_video_id = match[1].split('&').first - self.vimeo_video_id = nil - elsif self.video_url.include? 'youtu.be' - self.youtube_video_id = self.video_url.split('/').last - self.vimeo_video_id = nil - elsif self.video_url.blank? - self.vimeo_video_id = nil - self.youtube_video_id = nil - end - self - end + def total_raised + payments.sum(:gross_amount) + end - def total_raised - self.payments.sum(:gross_amount) - end + def percentage_funded + goal_amount.nil? ? 0 : total_raised * 100 / goal_amount + end - def percentage_funded - self.goal_amount.nil? ? 0 : self.total_raised * 100 / self.goal_amount - end + def average_donation + donations.any? ? total_raised / donations.count : 0 + end - def average_donation - self.donations.any? ? self.total_raised / self.donations.count : 0 - end - - # Validations + # Validations def end_datetime_cannot_be_in_past - if self.end_datetime.present? && self.end_datetime < Time.now + if end_datetime.present? && end_datetime < Time.now errors.add(:end_datetime, "can't be in the past") - end - end + end + end - def ready_to_publish? - [(self.body && self.body.length >= 500), (self.campaign_gift_options.count >= 1)].all? - end + def ready_to_publish? + [(body && body.length >= 500), (campaign_gift_options.count >= 1)].all? + end - def url - "#{self.nonprofit.url}/campaigns/#{self.slug}" - end + def url + "#{nonprofit.url}/campaigns/#{slug}" + end - def days_left - return 0 if self.end_datetime.nil? - (self.end_datetime.to_date - Date.today).to_i - end + def days_left + return 0 if end_datetime.nil? - def finished? - self.end_datetime && self.end_datetime < Time.now - end + (end_datetime.to_date - Date.today).to_i + end + + def finished? + end_datetime && end_datetime < Time.now + end def child_params - excluded_for_peer_to_peer = %w( - id created_at updated_at slug profile_id url + excluded_for_peer_to_peer = %w[ + id created_at updated_at slug profile_id url total_raised show_recurring_amount external_identifier parent_campaign_id reason_for_supporting metadata - ) + ] attributes.except(*excluded_for_peer_to_peer) end diff --git a/app/models/campaign_gift.rb b/app/models/campaign_gift.rb index d08a59da..af40b7f4 100644 --- a/app/models/campaign_gift.rb +++ b/app/models/campaign_gift.rb @@ -1,17 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGift < ApplicationRecord + # TODO + # attr_accessible \ + # :donation_id, + # :donation, + # :campaign_gift_option, + # :campaign_gift_option_id - #TODO - # attr_accessible \ - # :donation_id, - # :donation, - # :campaign_gift_option, - # :campaign_gift_option_id - - belongs_to :donation - belongs_to :campaign_gift_option - - validates :donation, presence: true - validates :campaign_gift_option, presence: true + belongs_to :donation + belongs_to :campaign_gift_option + validates :donation, presence: true + validates :campaign_gift_option, presence: true end diff --git a/app/models/campaign_gift_option.rb b/app/models/campaign_gift_option.rb index 187010d1..a8a1348c 100644 --- a/app/models/campaign_gift_option.rb +++ b/app/models/campaign_gift_option.rb @@ -1,36 +1,36 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGiftOption < ApplicationRecord + # TODO + # attr_accessible \ + # :amount_one_time, #int (cents) + # :amount_recurring, #int (cents) + # :amount_dollars, #str, gets converted to amount + # :description, # text + # :name, # str + # :campaign, #assocation + # :quantity, #int (optional) + # :to_ship, #boolean + # :order, #int (optional) + # :hide_contributions #boolean (optional) - #TODO - # attr_accessible \ - # :amount_one_time, #int (cents) - # :amount_recurring, #int (cents) - # :amount_dollars, #str, gets converted to amount - # :description, # text - # :name, # str - # :campaign, #assocation - # :quantity, #int (optional) - # :to_ship, #boolean - # :order, #int (optional) - # :hide_contributions #boolean (optional) + belongs_to :campaign + has_many :campaign_gifts + has_many :donations, through: :campaign_gifts - belongs_to :campaign - has_many :campaign_gifts - has_many :donations, through: :campaign_gifts + validates :name, presence: true + validates :campaign, presence: true + validates :amount_one_time, presence: true, numericality: { only_integer: true }, unless: :amount_recurring + validates :amount_recurring, presence: true, numericality: { only_integer: true }, unless: :amount_one_time - validates :name, presence: true - validates :campaign, presence: true - validates :amount_one_time, presence: true, numericality: { only_integer: true }, unless: :amount_recurring - validates :amount_recurring, presence: true, numericality: { only_integer: true }, unless: :amount_one_time - - def total_gifts - return self.campaign_gifts.count - end - - def as_json(options={}) - h = super(options) - h[:total_gifts] = self.total_gifts - h - end + def total_gifts + campaign_gifts.count + end + def as_json(options = {}) + h = super(options) + h[:total_gifts] = total_gifts + h + end end diff --git a/app/models/card.rb b/app/models/card.rb index 0c71e983..b5739b84 100755 --- a/app/models/card.rb +++ b/app/models/card.rb @@ -1,26 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Card < ApplicationRecord + # TODO + # attr_accessible \ + # :cardholders_name, # str (name associated with this card) + # :email, # str (cache the email associated with this card) + # :name, # str (readable card name, eg. Visa *1234) + # :failure_message, # accessor for temporarily storing the stripe decline message + # :status, # str + # :stripe_card_token, # str + # :stripe_card_id, # str + # :stripe_customer_id, # str + # :holder, :holder_id, :holder_type, # polymorphic cardholder association + # :inactive # a card is inactive. This is currently only meaningful for nonprofit cards - #TODO - # attr_accessible \ - # :cardholders_name, # str (name associated with this card) - # :email, # str (cache the email associated with this card) - # :name, # str (readable card name, eg. Visa *1234) - # :failure_message, # accessor for temporarily storing the stripe decline message - # :status, # str - # :stripe_card_token, # str - # :stripe_card_id, # str - # :stripe_customer_id, # str - # :holder, :holder_id, :holder_type, # polymorphic cardholder association - # :inactive # a card is inactive. This is currently only meaningful for nonprofit cards - - - attr_accessor :failure_message - - - belongs_to :holder, polymorphic: true - has_many :charges - has_many :donations - has_many :tickets + attr_accessor :failure_message + belongs_to :holder, polymorphic: true + has_many :charges + has_many :donations + has_many :tickets end diff --git a/app/models/charge.rb b/app/models/charge.rb index 42d7161d..d71c823a 100644 --- a/app/models/charge.rb +++ b/app/models/charge.rb @@ -1,37 +1,36 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # A Charge represents a potential debit to a nonprofit's account on a credit card donation action. class Charge < ApplicationRecord + # TODO + # attr_accessible \ + # :amount, + # :fee, + # :stripe_charge_id, + # :status - #TODO - # attr_accessible \ - # :amount, - # :fee, - # :stripe_charge_id, - # :status + has_one :campaign, through: :donation + has_one :recurring_donation, through: :donation + has_many :tickets + has_many :events, through: :tickets + has_many :refunds + has_many :disputes + belongs_to :supporter + belongs_to :card + belongs_to :direct_debit_detail + belongs_to :nonprofit + belongs_to :donation + belongs_to :payment + scope :paid, -> { where(status: %w[available pending disbursed]) } + scope :not_paid, -> { where(status: [nil, 'failed']) } + scope :available, -> { where(status: 'available') } + scope :pending, -> { where(status: 'pending') } + scope :disbursed, -> { where(status: 'disbursed') } - has_one :campaign, through: :donation - has_one :recurring_donation, through: :donation - has_many :tickets - has_many :events, through: :tickets - has_many :refunds - has_many :disputes - belongs_to :supporter - belongs_to :card - belongs_to :direct_debit_detail - belongs_to :nonprofit - belongs_to :donation - belongs_to :payment - - scope :paid, ->{where(status: ["available", "pending", "disbursed"])} - scope :not_paid, ->{where(status: [nil, "failed"])} - scope :available, ->{where(status: "available")} - scope :pending, ->{where(status: "pending")} - scope :disbursed, ->{where(status: "disbursed")} - - def paid? - self.status.in?(%w[available pending disbursed]) - end - + def paid? + status.in?(%w[available pending disbursed]) + end end diff --git a/app/models/comment.rb b/app/models/comment.rb index 8efbcc7b..1fc75312 100644 --- a/app/models/comment.rb +++ b/app/models/comment.rb @@ -1,36 +1,36 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Comment < ApplicationRecord + # TODO + # attr_accessible \ + # :host_id, :host_type, #parent: Event, Campaign, nil + # :profile_id, + # :body - #TODO - # attr_accessible \ - # :host_id, :host_type, #parent: Event, Campaign, nil - # :profile_id, - # :body + validates :profile, presence: true + validates :body, presence: true, length: { maximum: 200 } - validates :profile, :presence => true - validates :body, :presence => true, :length => {:maximum => 200} + has_one :activity, as: :attachment, dependent: :destroy + belongs_to :host, polymorphic: true + belongs_to :donation + belongs_to :profile - has_one :activity, :as => :attachment, :dependent => :destroy - belongs_to :host, :polymorphic => true - belongs_to :donation - belongs_to :profile + before_validation(on: :create) do + remove_newlines + end - before_validation(:on => :create) do - remove_newlines - end - - after_create do - self.create_activity({ - :desc => 'commented', - :profile_id => self.profile_id, - :host_id => self.host_id, - :host_type => self.host_type, - :body => self.body - }) - end - - def remove_newlines - self.body = self.body && self.body.gsub(/\n/,'') - end + after_create do + create_activity( + desc: 'commented', + profile_id: profile_id, + host_id: host_id, + host_type: host_type, + body: body + ) + end + def remove_newlines + self.body = body && body.delete("\n") + end end diff --git a/app/models/coupon.rb b/app/models/coupon.rb index 5b8b5f7c..0453c9e3 100644 --- a/app/models/coupon.rb +++ b/app/models/coupon.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Coupon < ApplicationRecord - # attr_accessible \ - # :name, - # :victim_np_id, - # :paid, # boolean - # :nonprofit, :nonprofit_id + # attr_accessible \ + # :name, + # :victim_np_id, + # :paid, # boolean + # :nonprofit, :nonprofit_id - scope :unpaid, -> {where(paid: [nil,false])} + scope :unpaid, -> { where(paid: [nil, false]) } - validates_presence_of :name, :nonprofit_id, :victim_np_id + validates_presence_of :name, :nonprofit_id, :victim_np_id end diff --git a/app/models/custom_field_join.rb b/app/models/custom_field_join.rb index fb915709..fcd27739 100644 --- a/app/models/custom_field_join.rb +++ b/app/models/custom_field_join.rb @@ -1,26 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CustomFieldJoin < ApplicationRecord + # TODO + # attr_accessible \ + # :supporter, :supporter_id, + # :custom_field_master, :custom_field_master_id, + # :value - #TODO - # attr_accessible \ - # :supporter, :supporter_id, - # :custom_field_master, :custom_field_master_id, - # :value + validates :custom_field_master, presence: true - validates :custom_field_master, presence: true - - belongs_to :custom_field_master + belongs_to :custom_field_master belongs_to :supporter - def self.create_with_name(nonprofit, h) - cfm = nonprofit.custom_field_masters.find_by_name(h['name']) - if cfm.nil? - cfm = nonprofit.custom_field_masters.create(name: h['name']) - end - self.create({value: h['value'], custom_field_master_id: cfm.id, supporter_id: h['supporter_id']}) - end - - def name; custom_field_master.name; end + def self.create_with_name(nonprofit, h) + cfm = nonprofit.custom_field_masters.find_by_name(h['name']) + cfm = nonprofit.custom_field_masters.create(name: h['name']) if cfm.nil? + create(value: h['value'], custom_field_master_id: cfm.id, supporter_id: h['supporter_id']) + end + def name + custom_field_master.name +end end - diff --git a/app/models/custom_field_master.rb b/app/models/custom_field_master.rb index b528b373..d2e386c0 100644 --- a/app/models/custom_field_master.rb +++ b/app/models/custom_field_master.rb @@ -1,24 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CustomFieldMaster < ApplicationRecord + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :created_at - # attr_accessible \ - # :nonprofit, :nonprofit_id, - # :name, - # :deleted, - # :created_at + validates :name, presence: true + validate :no_dupes, on: :create - validates :name, presence: true - validate :no_dupes, on: :create + belongs_to :nonprofit + has_many :custom_field_joins, dependent: :destroy - belongs_to :nonprofit - has_many :custom_field_joins, dependent: :destroy + scope :not_deleted, -> { where(deleted: [nil, false]) } - scope :not_deleted, ->{where(deleted: [nil,false])} - - def no_dupes - return self if nonprofit.nil? - errors.add(:base, "Duplicate custom field") if nonprofit.custom_field_masters.not_deleted.where(name: name).any? - end + def no_dupes + return self if nonprofit.nil? + errors.add(:base, 'Duplicate custom field') if nonprofit.custom_field_masters.not_deleted.where(name: name).any? + end end - diff --git a/app/models/direct_debit_detail.rb b/app/models/direct_debit_detail.rb index a8e96676..59efae5e 100644 --- a/app/models/direct_debit_detail.rb +++ b/app/models/direct_debit_detail.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class DirectDebitDetail < ApplicationRecord # attr_accessible :iban, :account_holder_name, :bic, :supporter_id, :holder diff --git a/app/models/dispute.rb b/app/models/dispute.rb index 78bd2754..aa01fb80 100644 --- a/app/models/dispute.rb +++ b/app/models/dispute.rb @@ -1,11 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Dispute < ApplicationRecord + Reasons = %i[unrecognized duplicate fraudulent subscription_canceled product_unacceptable product_not_received unrecognized credit_not_processed goods_services_returned_or_refused goods_services_cancelled incorrect_account_details insufficient_funds bank_cannot_process debit_not_authorized general].freeze - Reasons = [:unrecognized, :duplicate, :fraudulent, :subscription_canceled, :product_unacceptable, :product_not_received, :unrecognized, :credit_not_processed, :goods_services_returned_or_refused, :goods_services_cancelled, :incorrect_account_details, :insufficient_funds, :bank_cannot_process, :debit_not_authorized, :general] - - - Statuses = [:needs_response, :under_review, :won, :lost, :lost_and_paid] - #TODO + Statuses = %i[needs_response under_review won lost lost_and_paid].freeze + # TODO # attr_accessible \ # :gross_amount, # int # :charge_id, :charge, @@ -15,6 +15,4 @@ class Dispute < ApplicationRecord belongs_to :charge belongs_to :payment - end - diff --git a/app/models/donation.rb b/app/models/donation.rb index b830b8b6..d5b77719 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,49 +1,50 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Donation < ApplicationRecord + # TODO + # attr_accessible \ + # :date, # datetime (when this donation was made) + # :amount, # int (in cents) + # :recurring, # bool + # :anonymous, # bool + # :email, # str (cached email of the donor) + # :designation, # text + # :dedication, # text + # :comment, # text + # :origin_url, # text + # :nonprofit_id, :nonprofit, + # :card_id, :card, # Card with which any charges were made + # :supporter_id, :supporter, + # :profile_id, :profile, + # :campaign_id, :campaign, + # :payment_id, :payment, + # :event_id, :event, + # :direct_debit_detail_id, :direct_debit_detail, + # :payment_provider - #TODO - # attr_accessible \ - # :date, # datetime (when this donation was made) - # :amount, # int (in cents) - # :recurring, # bool - # :anonymous, # bool - # :email, # str (cached email of the donor) - # :designation, # text - # :dedication, # text - # :comment, # text - # :origin_url, # text - # :nonprofit_id, :nonprofit, - # :card_id, :card, # Card with which any charges were made - # :supporter_id, :supporter, - # :profile_id, :profile, - # :campaign_id, :campaign, - # :payment_id, :payment, - # :event_id, :event, - # :direct_debit_detail_id, :direct_debit_detail, - # :payment_provider + validates :amount, presence: true, numericality: { only_integer: true } + validates :supporter, presence: true + validates :nonprofit, presence: true + validates_associated :charges + validates :payment_provider, inclusion: { in: %w[credit_card sepa] }, allow_blank: true - validates :amount, presence: true, numericality: { only_integer: true } - validates :supporter, presence: true - validates :nonprofit, presence: true - validates_associated :charges - validates :payment_provider, inclusion: { in: ["credit_card", "sepa"]}, allow_blank: true + has_many :charges + has_many :campaign_gifts, dependent: :destroy + has_many :campaign_gift_options, through: :campaign_gifts + has_many :activities, as: :attachment, dependent: :destroy + has_many :payments + has_one :recurring_donation + has_one :payment + has_one :offsite_payment + has_one :tracking + belongs_to :supporter + belongs_to :card + belongs_to :direct_debit_detail + belongs_to :profile + belongs_to :nonprofit + belongs_to :campaign + belongs_to :event - has_many :charges - has_many :campaign_gifts, dependent: :destroy - has_many :campaign_gift_options, through: :campaign_gifts - has_many :activities, as: :attachment, dependent: :destroy - has_many :payments - has_one :recurring_donation - has_one :payment - has_one :offsite_payment - has_one :tracking - belongs_to :supporter - belongs_to :card - belongs_to :direct_debit_detail - belongs_to :profile - belongs_to :nonprofit - belongs_to :campaign - belongs_to :event - - scope :anonymous, -> {where(anonymous: true)} + scope :anonymous, -> { where(anonymous: true) } end diff --git a/app/models/email_draft.rb b/app/models/email_draft.rb index 7de0e030..ed7047dd 100644 --- a/app/models/email_draft.rb +++ b/app/models/email_draft.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailDraft < ApplicationRecord - #TODO - # attr_accessible \ - # :nonprofit, :nonprofit_id, - # :name, - # :deleted, - # :value, - # :created_at + # TODO + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :value, + # :created_at - belongs_to :nonprofit - - scope :not_deleted, ->{where(deleted: [nil,false])} + belongs_to :nonprofit + scope :not_deleted, -> { where(deleted: [nil, false]) } end - diff --git a/app/models/email_list.rb b/app/models/email_list.rb index f60af7d0..80979b51 100644 --- a/app/models/email_list.rb +++ b/app/models/email_list.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailList < ApplicationRecord # attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master diff --git a/app/models/email_setting.rb b/app/models/email_setting.rb index d81db281..1f6a19e3 100644 --- a/app/models/email_setting.rb +++ b/app/models/email_setting.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailSetting < ApplicationRecord - - #TODO + # TODO # attr_accessible \ # :user_id, :user, # :nonprofit_id, :nonprofit, @@ -13,5 +14,4 @@ class EmailSetting < ApplicationRecord belongs_to :nonprofit belongs_to :user - end diff --git a/app/models/event.rb b/app/models/event.rb index 0e88dc03..bba09a11 100644 --- a/app/models/event.rb +++ b/app/models/event.rb @@ -1,105 +1,102 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Event < ApplicationRecord + # TODO + # attr_accessible \ + # :deleted, #bool for soft-delete + # :name, # str + # :tagline, # str + # :summary, # text + # :body, # text (html) + # :end_datetime, + # :start_datetime, + # :latitude, # float + # :longitude, # float + # :location, # str + # :city, # str + # :state_code, # str + # :address, # str + # :zip_code, # str + # :main_image, # str + # :remove_main_image, # for carrierwave + # :background_image, # str + # :remove_background_image, # bool carrierwave + # :published, # bool + # :slug, # str + # :directions, # text + # :venue_name, # str + # :profile_id, # creator + # :ticket_levels_attributes, + # :show_total_raised, # bool + # :show_total_count, # bool + # :hide_activity_feed, # bool + # :nonprofit_id, # host + # :hide_title, # bool + # :organizer_email, # string + # :receipt_message # text - #TODO - # attr_accessible \ - # :deleted, #bool for soft-delete - # :name, # str - # :tagline, # str - # :summary, # text - # :body, # text (html) - # :end_datetime, - # :start_datetime, - # :latitude, # float - # :longitude, # float - # :location, # str - # :city, # str - # :state_code, # str - # :address, # str - # :zip_code, # str - # :main_image, # str - # :remove_main_image, # for carrierwave - # :background_image, # str - # :remove_background_image, # bool carrierwave - # :published, # bool - # :slug, # str - # :directions, # text - # :venue_name, # str - # :profile_id, # creator - # :ticket_levels_attributes, - # :show_total_raised, # bool - # :show_total_count, # bool - # :hide_activity_feed, # bool - # :nonprofit_id, # host - # :hide_title, # bool - # :organizer_email, # string - # :receipt_message # text + validates :name, presence: true + validates :end_datetime, presence: true + validates :start_datetime, presence: true + validates :address, presence: true + validates :city, presence: true + validates :state_code, presence: true + validates :slug, presence: true, uniqueness: { scope: :nonprofit_id, message: 'You already have an event with that URL' } + validates :nonprofit_id, presence: true + validates :profile_id, presence: true - validates :name, :presence => true - validates :end_datetime, :presence => true - validates :start_datetime, :presence => true - validates :address, :presence => true - validates :city, :presence => true - validates :state_code, :presence => true - validates :slug, :presence => true, uniqueness: {scope: :nonprofit_id, message: 'You already have an event with that URL'} - validates :nonprofit_id, :presence => true - validates :profile_id, :presence => true - - belongs_to :nonprofit - belongs_to :profile - has_many :donations - has_many :charges, through: :tickets - has_many :supporters, through: :donations - has_many :recurring_donations - has_many :ticket_levels, :dependent => :destroy + belongs_to :nonprofit + belongs_to :profile + has_many :donations + has_many :charges, through: :tickets + has_many :supporters, through: :donations + has_many :recurring_donations + has_many :ticket_levels, dependent: :destroy has_many :event_discounts, dependent: :destroy - has_many :tickets - has_many :payments, through: :tickets - has_many :roles, as: :host, dependent: :destroy - has_many :comments, as: :host, dependent: :destroy - has_many :activities, as: :host, dependent: :destroy + has_many :tickets + has_many :payments, through: :tickets + has_many :roles, as: :host, dependent: :destroy + has_many :comments, as: :host, dependent: :destroy + has_many :activities, as: :host, dependent: :destroy + geocoded_by :full_address - geocoded_by :full_address + accepts_nested_attributes_for :ticket_levels, allow_destroy: true - accepts_nested_attributes_for :ticket_levels, :allow_destroy => true + mount_uploader :main_image, EventMainImageUploader + mount_uploader :background_image, EventBackgroundImageUploader - mount_uploader :main_image, EventMainImageUploader - mount_uploader :background_image, EventBackgroundImageUploader + scope :not_deleted, -> { where(deleted: [nil, false]) } + scope :deleted, -> { where(deleted: true) } + scope :published, -> { where(published: true) } + scope :upcoming, -> { where('start_datetime >= ?', Date.today).published } + scope :past, -> { where('end_datetime < ?', Date.today).published } + scope :unpublished, -> { where('published != ?', true) } - scope :not_deleted, -> {where(deleted: [nil,false])} - scope :deleted, -> {where(deleted: true)} - scope :published, -> {where(:published => true)} - scope :upcoming, -> {where("start_datetime >= ?", Date.today).published} - scope :past, -> {where("end_datetime < ?", Date.today).published} - scope :unpublished, -> {where("published != ?", true)} + validates :slug, uniqueness: { scope: :nonprofit_id, message: 'You already have a campaign with that name.' } - validates :slug, uniqueness: {scope: :nonprofit_id, message: 'You already have a campaign with that name.'} - - before_validation(on: :create) do - unless self.slug - self.slug = Format::Url.convert_to_slug(name) - end - self.published = false if self.published.nil? - self.total_raised ||= 0 + before_validation(on: :create) do + self.slug = Format::Url.convert_to_slug(name) unless slug + self.published = false if published.nil? + self.total_raised ||= 0 self - end - - after_validation :geocode - - after_create do - user = self.profile.user - Role.create(name: :event_editor, user_id: user.id, host: self) - EventMailer.delay.creation_followup(self) - self - end - - def url - "#{self.nonprofit.url}/events/#{self.slug}" - end - - def full_address - Format::Address.full_address(self.address, self.city, self.state_code, self.zip_code) end + after_validation :geocode + + after_create do + user = profile.user + Role.create(name: :event_editor, user_id: user.id, host: self) + EventMailer.delay.creation_followup(self) + self + end + + def url + "#{nonprofit.url}/events/#{slug}" + end + + def full_address + Format::Address.full_address(address, city, state_code, zip_code) + end end diff --git a/app/models/event_discount.rb b/app/models/event_discount.rb index d09d5c9b..7f7d9e15 100644 --- a/app/models/event_discount.rb +++ b/app/models/event_discount.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventDiscount < ApplicationRecord - #TODO + # TODO # attr_accessible \ # :code, # :event_id, @@ -9,5 +11,4 @@ class EventDiscount < ApplicationRecord belongs_to :event has_many :tickets - end diff --git a/app/models/export.rb b/app/models/export.rb index 83f066c2..c7a3a773 100644 --- a/app/models/export.rb +++ b/app/models/export.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Export < ApplicationRecord - STATUS = %w[queued started completed failed].freeze # attr_accessible :exception, :nonprofit, :status, :user, :export_type, :parameters, :ended, :url, :user_id, :nonprofit_id diff --git a/app/models/full_contact_info.rb b/app/models/full_contact_info.rb index 2035a618..501fbc92 100644 --- a/app/models/full_contact_info.rb +++ b/app/models/full_contact_info.rb @@ -1,24 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactInfo < ApplicationRecord - #TODO - # attr_accessible \ - # :email, - # :full_name, - # :gender, - # :city, - # :county, - # :state_code, - # :country, - # :continent, - # :age, - # :age_range, - # :location_general, - # :supporter_id, :supporter, - # :websites + # TODO + # attr_accessible \ + # :email, + # :full_name, + # :gender, + # :city, + # :county, + # :state_code, + # :country, + # :continent, + # :age, + # :age_range, + # :location_general, + # :supporter_id, :supporter, + # :websites - has_many :full_contact_photos - has_many :full_contact_social_profiles - has_many :full_contact_orgs - has_many :full_contact_topics + has_many :full_contact_photos + has_many :full_contact_social_profiles + has_many :full_contact_orgs + has_many :full_contact_topics belongs_to :supporter end diff --git a/app/models/full_contact_org.rb b/app/models/full_contact_org.rb index 2f0b0a43..f457b5bc 100644 --- a/app/models/full_contact_org.rb +++ b/app/models/full_contact_org.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactOrg < ApplicationRecord - - #TODO - # attr_accessible \ + # TODO + # attr_accessible \ # :name, # :is_primary, # :name, @@ -13,5 +14,4 @@ class FullContactOrg < ApplicationRecord # :full_contact_info_id, :full_contact_info belongs_to :full_contact_info - end diff --git a/app/models/full_contact_photo.rb b/app/models/full_contact_photo.rb index 181b92fe..de86b272 100644 --- a/app/models/full_contact_photo.rb +++ b/app/models/full_contact_photo.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactPhoto < ApplicationRecord - #TODO - # attr_accessible \ - # :full_contact_info, - # :full_contact_info_id, - # :type_id, # i.e. twitter, linkedin, facebook - # :is_primary, #bool - # :url #string + # TODO + # attr_accessible \ + # :full_contact_info, + # :full_contact_info_id, + # :type_id, # i.e. twitter, linkedin, facebook + # :is_primary, #bool + # :url #string - belongs_to :full_contact_info + belongs_to :full_contact_info - validates_presence_of :full_contact_info -end \ No newline at end of file + validates_presence_of :full_contact_info +end diff --git a/app/models/full_contact_social_profile.rb b/app/models/full_contact_social_profile.rb index b85511dd..4cdacf24 100644 --- a/app/models/full_contact_social_profile.rb +++ b/app/models/full_contact_social_profile.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactSocialProfile < ApplicationRecord - #TODO - # attr_accessible \ - # :full_contact_info, - # :full_contact_info_id, - # :type_id, # i.e. twitter, linkedin, facebook - # :username, #string - # :uid, # string - # :bio, #string - # :url #string + # TODO + # attr_accessible \ + # :full_contact_info, + # :full_contact_info_id, + # :type_id, # i.e. twitter, linkedin, facebook + # :username, #string + # :uid, # string + # :bio, #string + # :url #string - belongs_to :full_contact_info + belongs_to :full_contact_info - validates_presence_of :full_contact_info -end \ No newline at end of file + validates_presence_of :full_contact_info +end diff --git a/app/models/full_contact_topic.rb b/app/models/full_contact_topic.rb index 5dcdbf97..98f6fe37 100644 --- a/app/models/full_contact_topic.rb +++ b/app/models/full_contact_topic.rb @@ -1,12 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class FullContactTopic < ApplicationRecord - - #TODO - # attr_accessible \ + # TODO + # attr_accessible \ # :provider, # :value, # :full_contact_info_id, :full_contact_info belongs_to :full_contact_info - end diff --git a/app/models/image_attachment.rb b/app/models/image_attachment.rb index 642ba68c..ce97e086 100644 --- a/app/models/image_attachment.rb +++ b/app/models/image_attachment.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ImageAttachment < ApplicationRecord + attr_accessible :parent_id, :file + mount_uploader :file, ImageAttachmentUploader - attr_accessible :parent_id, :file - mount_uploader :file, ImageAttachmentUploader - - # not sure if poly parent is used on this model, as all values are nil in db - belongs_to :parent, :polymorphic => true - + # not sure if poly parent is used on this model, as all values are nil in db + belongs_to :parent, polymorphic: true end diff --git a/app/models/import.rb b/app/models/import.rb index 4c056f1e..8a93f4c6 100644 --- a/app/models/import.rb +++ b/app/models/import.rb @@ -1,20 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Import < ApplicationRecord + # TODO + # attr_accessible \ + # :user_id, :user, + # :email, # email of the user who ma + # :nonprofit_id, :nonprofit, + # :row_count, + # :imported_count, + # :date - #TODO - # attr_accessible \ - # :user_id, :user, - # :email, # email of the user who ma - # :nonprofit_id, :nonprofit, - # :row_count, - # :imported_count, - # :date - - has_many :supporters - belongs_to :nonprofit - belongs_to :user - - validates :user, presence: true + has_many :supporters + belongs_to :nonprofit + belongs_to :user + validates :user, presence: true end - diff --git a/app/models/miscellaneous_np_info.rb b/app/models/miscellaneous_np_info.rb index 240d6478..6a15fee8 100644 --- a/app/models/miscellaneous_np_info.rb +++ b/app/models/miscellaneous_np_info.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class MiscellaneousNpInfo < ApplicationRecord - - #TODO + # TODO # attr_accessible \ # :donate_again_url, # :change_amount_message diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index f03781f1..25c0ad29 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Nonprofit < ApplicationRecord + Categories = ['Public Benefit', 'Human Services', 'Education', 'Civic Duty', 'Human Rights', 'Animals', 'Environment', 'Health', 'Arts, Culture, Humanities', 'International', 'Children', 'Religion', 'LGBTQ', "Women's Rights", 'Disaster Relief', 'Veterans'].freeze - Categories = ["Public Benefit", "Human Services", "Education", "Civic Duty", "Human Rights", "Animals", "Environment", "Health", "Arts, Culture, Humanities", "International", "Children", "Religion", "LGBTQ", "Women's Rights", "Disaster Relief", "Veterans"] - - #TODO + # TODO # attr_accessible \ # :name, # str # :stripe_account_id, # str @@ -74,7 +75,7 @@ class Nonprofit < ApplicationRecord has_many :email_settings has_many :cards, as: :holder - has_one :bank_account, -> { where("COALESCE(deleted, false) = false") }, + has_one :bank_account, -> { where('COALESCE(deleted, false) = false') }, dependent: :destroy has_one :billing_subscription, dependent: :destroy has_one :billing_plan, through: :billing_subscription @@ -84,12 +85,12 @@ class Nonprofit < ApplicationRecord validates :city, presence: true validates :state_code, presence: true validates :email, format: { with: Email::Regex }, allow_blank: true - validates_uniqueness_of :slug, scope: [:city_slug, :state_code_slug] + validates_uniqueness_of :slug, scope: %i[city_slug state_code_slug] validates_presence_of :slug - scope :vetted, -> {where(vetted: true)} - scope :identity_verified, -> {where(verification_status: 'verified')} - scope :published, -> {where(published: true)} + scope :vetted, -> { where(vetted: true) } + scope :identity_verified, -> { where(verification_status: 'verified') } + scope :published, -> { where(published: true) } mount_uploader :main_image, NonprofitUploader mount_uploader :second_image, NonprofitUploader @@ -103,20 +104,19 @@ class Nonprofit < ApplicationRecord geocoded_by :full_address before_validation(on: :create) do - self.set_slugs + set_slugs self end # Register (create) a nonprofit with an initial admin def self.register(user, params) - np = self.create ConstructNonprofit.construct(user, params) + np = create ConstructNonprofit.construct(user, params) role = Role.create(user: user, name: 'nonprofit_admin', host: np) if np.valid? - return np + np end - def nonprofit_personnel_emails - self.roles.nonprofit_personnel.joins(:user).pluck('users.email') + roles.nonprofit_personnel.joins(:user).pluck('users.email') end def total_recurring @@ -125,62 +125,59 @@ class Nonprofit < ApplicationRecord def donation_history_monthly donation_history_monthly = [] - donations.order("created_at") - .group_by{|d| d.created_at.beginning_of_month} - .each{|_, ds| donation_history_monthly.push(ds.map(&:amount).sum)} + donations.order('created_at') + .group_by { |d| d.created_at.beginning_of_month } + .each { |_, ds| donation_history_monthly.push(ds.map(&:amount).sum) } donation_history_monthly end def as_json(options = {}) h = super(options) - h[:url] = self.url + h[:url] = url h end def url - "/#{self.state_code_slug}/#{self.city_slug}/#{self.slug}" + "/#{state_code_slug}/#{city_slug}/#{slug}" end def set_slugs - unless (self.slug) - self.slug = Format::Url.convert_to_slug self.name - end - unless (self.city_slug) - self.city_slug = Format::Url.convert_to_slug self.city - end + self.slug = Format::Url.convert_to_slug name unless slug + self.city_slug = Format::Url.convert_to_slug city unless city_slug - unless (self.state_code_slug) - self.state_code_slug = Format::Url.convert_to_slug self.state_code + unless state_code_slug + self.state_code_slug = Format::Url.convert_to_slug state_code end self end def full_address - Format::Address.full_address(self.address, self.city, self.state_code) + Format::Address.full_address(address, city, state_code) end def total_raised - QueryPayments.get_payout_totals( QueryPayments.ids_for_payout(self.id))['net_amount'] + QueryPayments.get_payout_totals(QueryPayments.ids_for_payout(id))['net_amount'] end def can_make_payouts - self.vetted && - self.verification_status == 'verified' && - self.bank_account && - !self.bank_account.pending_verification + vetted && + verification_status == 'verified' && + bank_account && + !bank_account.pending_verification end def active_cards - cards.where("COALESCE(cards.inactive, FALSE) = FALSE") + cards.where('COALESCE(cards.inactive, FALSE) = FALSE') end # @param [Card] card the new active_card def active_card=(card) unless card.class == Card - raise ArgumentError.new "Pass a card to active_card or else" + raise ArgumentError, 'Pass a card to active_card or else' end + Card.transaction do - active_cards.update_all :inactive => true + active_cards.update_all inactive: true return cards << card end end @@ -190,14 +187,15 @@ class Nonprofit < ApplicationRecord end def create_active_card(card_data) - if (card_data[:inactive]) - raise ArgumentError.new "This method is for creating active cards only" + if card_data[:inactive] + raise ArgumentError, 'This method is for creating active cards only' end - active_cards.update_all :inactive => true - return cards.create(card_data) + + active_cards.update_all inactive: true + cards.create(card_data) end def currency_symbol - Settings.intntl.all_currencies.find{|i| i.abbv.downcase == currency.downcase}&.symbol + Settings.intntl.all_currencies.find { |i| i.abbv.casecmp(currency).zero? }&.symbol end end diff --git a/app/models/nonprofit_account.rb b/app/models/nonprofit_account.rb index b4f67ffe..5410c269 100644 --- a/app/models/nonprofit_account.rb +++ b/app/models/nonprofit_account.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitAccount < ApplicationRecord + # TODO + # attr_accessible \ + # :stripe_account_id, #str + # :nonprofit, :nonprofit_id #int - #TODO - # attr_accessible \ - # :stripe_account_id, #str - # :nonprofit, :nonprofit_id #int - - belongs_to :nonprofit - - validates :nonprofit, presence: true - validates :stripe_account_id, presence: true + belongs_to :nonprofit + validates :nonprofit, presence: true + validates :stripe_account_id, presence: true end diff --git a/app/models/offsite_payment.rb b/app/models/offsite_payment.rb index 798ef1c9..abdbcd37 100644 --- a/app/models/offsite_payment.rb +++ b/app/models/offsite_payment.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class OffsitePayment < ApplicationRecord - - # attr_accessible :gross_amount, :kind, :date, :check_number - belongs_to :payment, dependent: :destroy - belongs_to :donation - belongs_to :nonprofit - belongs_to :supporter - + # attr_accessible :gross_amount, :kind, :date, :check_number + belongs_to :payment, dependent: :destroy + belongs_to :donation + belongs_to :nonprofit + belongs_to :supporter end diff --git a/app/models/payment.rb b/app/models/payment.rb index be3d68e8..f355d8f2 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -1,30 +1,30 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # A payment represents the event where a nonprofit receives money from a supporter # If connected to a charge, this represents money potentially debited to the nonprofit's account # If connected to an offsite_payment, this is money the nonprofit is recording for convenience. class Payment < ApplicationRecord + # TODO + # attr_accessible \ + # :towards, + # :gross_amount, + # :refund_total, + # :fee_total, + # :kind, + # :date -#TODO -# attr_accessible \ -# :towards, -# :gross_amount, -# :refund_total, -# :fee_total, -# :kind, -# :date - - belongs_to :supporter - belongs_to :nonprofit - has_one :charge - has_one :offsite_payment - has_one :refund - has_one :dispute - belongs_to :donation - has_many :tickets - has_one :campaign, through: :donation - has_many :events, through: :tickets - has_many :payment_payouts - has_many :charges - + belongs_to :supporter + belongs_to :nonprofit + has_one :charge + has_one :offsite_payment + has_one :refund + has_one :dispute + belongs_to :donation + has_many :tickets + has_one :campaign, through: :donation + has_many :events, through: :tickets + has_many :payment_payouts + has_many :charges end diff --git a/app/models/payment_import.rb b/app/models/payment_import.rb index 5980928f..4d9fa279 100644 --- a/app/models/payment_import.rb +++ b/app/models/payment_import.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class PaymentImport < ApplicationRecord # attr_accessible :nonprofit, :user diff --git a/app/models/payment_payout.rb b/app/models/payment_payout.rb index b3b01465..f5dc5354 100644 --- a/app/models/payment_payout.rb +++ b/app/models/payment_payout.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # charge_payouts are a join table between charges and payouts # @@ -12,19 +14,17 @@ # since our fees will continue to change as our transaction volume increases class PaymentPayout < ApplicationRecord + # TODO + # attr_accessible \ + # :payment_id, :payment, + # :charge_id, :charge, # deprecated + # :payout_id, :payout, + # :total_fees # int (cents) - #TODO - # attr_accessible \ - # :payment_id, :payment, - # :charge_id, :charge, # deprecated - # :payout_id, :payout, - # :total_fees # int (cents) + belongs_to :charge # deprecated + belongs_to :payment + belongs_to :payout - belongs_to :charge # deprecated - belongs_to :payment - belongs_to :payout - - validates :payment, presence: true - validates :payout, presence: true + validates :payment, presence: true + validates :payout, presence: true end - diff --git a/app/models/payout.rb b/app/models/payout.rb index 6529ea1d..26222453 100644 --- a/app/models/payout.rb +++ b/app/models/payout.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Payouts record a credit of the total pending balance on a nonprofit's account # to their bank account or debit card @@ -5,54 +7,50 @@ # These are tied to Stripe transfers class Payout < ApplicationRecord + # TODO + # attr_accessible \ + # :scheduled, # bool (whether this was made automatically at the beginning of the month) + # :count, # int (number of donations for this payout) + # :ach_fee, # int (in cents, the total fee for the payout itself) + # :gross_amount, # int (in cents, total amount before fees) + # :fee_total, # int (in cents, total amount of fees) + # :net_amount, # int (in cents, total amount after fees for this payout) + # :email, # str (cache of user email who issued this) + # :user_ip, # str (ip address of the user who made this payout) + # :status, # str ('pending', 'paid', 'canceled', or 'failed') + # :failure_message, # str + # :bank_name, # str: cache of the nonprofit's bank name + # :stripe_transfer_id, # str + # :nonprofit_id, :nonprofit - #TODO - # attr_accessible \ - # :scheduled, # bool (whether this was made automatically at the beginning of the month) - # :count, # int (number of donations for this payout) - # :ach_fee, # int (in cents, the total fee for the payout itself) - # :gross_amount, # int (in cents, total amount before fees) - # :fee_total, # int (in cents, total amount of fees) - # :net_amount, # int (in cents, total amount after fees for this payout) - # :email, # str (cache of user email who issued this) - # :user_ip, # str (ip address of the user who made this payout) - # :status, # str ('pending', 'paid', 'canceled', or 'failed') - # :failure_message, # str - # :bank_name, # str: cache of the nonprofit's bank name - # :stripe_transfer_id, # str - # :nonprofit_id, :nonprofit + belongs_to :nonprofit + has_one :bank_account, through: :nonprofit + has_many :payment_payouts + has_many :payments, through: :payment_payouts - belongs_to :nonprofit - has_one :bank_account, through: :nonprofit - has_many :payment_payouts - has_many :payments, through: :payment_payouts + validates :stripe_transfer_id, presence: true, uniqueness: true + validates :nonprofit, presence: true + validates :bank_account, presence: true + validates :email, presence: true + validates :net_amount, presence: true, numericality: { greater_than: 0 } + validate :nonprofit_must_be_vetted, on: :create + validate :nonprofit_must_have_identity_verified, on: :create + validate :bank_account_must_be_confirmed, on: :create - validates :stripe_transfer_id, presence: true, uniqueness: true - validates :nonprofit, presence: true - validates :bank_account, presence: true - validates :email, presence: true - validates :net_amount, presence: true, numericality: {greater_than: 0} - validate :nonprofit_must_be_vetted, on: :create - validate :nonprofit_must_have_identity_verified, on: :create - validate :bank_account_must_be_confirmed, on: :create + scope :pending, -> { where(status: 'pending') } + scope :paid, -> { where(status: %w[paid succeeded]) } - scope :pending, -> {where(status: 'pending')} - scope :paid, -> {where(status: ['paid', 'succeeded'])} + def bank_account_must_be_confirmed + if bank_account&.pending_verification + errors.add(:bank_account, 'must be confirmed via email') + end + end + def nonprofit_must_have_identity_verified + errors.add(:nonprofit, 'must be verified') unless nonprofit && nonprofit.verification_status == 'verified' + end - def bank_account_must_be_confirmed - if self.bank_account && self.bank_account.pending_verification - self.errors.add(:bank_account, 'must be confirmed via email') - end - end - - def nonprofit_must_have_identity_verified - self.errors.add(:nonprofit, "must be verified") unless self.nonprofit && self.nonprofit.verification_status == 'verified' - end - - def nonprofit_must_be_vetted - self.errors.add(:nonprofit, "must be vetted") unless self.nonprofit && self.nonprofit.vetted - end - + def nonprofit_must_be_vetted + errors.add(:nonprofit, 'must be vetted') unless nonprofit&.vetted + end end - diff --git a/app/models/profile.rb b/app/models/profile.rb index be081dd6..3dba93d8 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -1,120 +1,121 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Profile < ApplicationRecord + # TODO + # attr_accessible \ + # :registered, # bool + # :mini_bio, + # :first_name, # str + # :last_name, # str + # :name, + # :phone, # str + # :address, # str + # :email, # str + # :city, # str + # :state_code, # str (eg. CA) + # :zip_code, # str + # :privacy_settings, # text [str]: XXX deprecated + # :picture, # str: either their social network pic or a stored pic on S3 + # :anonymous, # bool: negates all privacy_settings + # :city_state, + # :user_id - #TODO - # attr_accessible \ - # :registered, # bool - # :mini_bio, - # :first_name, # str - # :last_name, # str - # :name, - # :phone, # str - # :address, # str - # :email, # str - # :city, # str - # :state_code, # str (eg. CA) - # :zip_code, # str - # :privacy_settings, # text [str]: XXX deprecated - # :picture, # str: either their social network pic or a stored pic on S3 - # :anonymous, # bool: negates all privacy_settings - # :city_state, - # :user_id + validates :email, format: { with: Email::Regex }, allow_blank: true - validates :email, format: {with: Email::Regex}, allow_blank: true + attr_accessor :email, :city_state - attr_accessor :email, :city_state + serialize :privacy_settings, Array - serialize :privacy_settings, Array + mount_uploader :picture, ProfileUploader - mount_uploader :picture, ProfileUploader + belongs_to :user + has_many :activities # Activities this profile has created + has_many :supporters + has_many :donations + has_many :campaigns + has_many :events + has_many :recurring_donations + has_many :comments, as: :host, dependent: :destroy + has_many :nonprofits, through: :supporters + has_many :activities, dependent: :destroy + # has_one :card, as: :holder - belongs_to :user - has_many :activities # Activities this profile has created - has_many :supporters - has_many :donations - has_many :campaigns - has_many :events - has_many :recurring_donations - has_many :comments, as: :host, dependent: :destroy - has_many :nonprofits, through: :supporters - has_many :activities, dependent: :destroy -# has_one :card, as: :holder + # accepts_nested_attributes_for :card - #accepts_nested_attributes_for :card + scope :non_anon, -> { where(anonymous: [nil, false]) } - scope :non_anon, -> {where(anonymous: [nil, false])} + before_validation(on: :create) do + set_defaults + self + end - before_validation(on: :create) do - self.set_defaults - self - end + def set_defaults + self.name ||= user.name if user + self.email ||= user.email if user + self.picture ||= user.picture if user + if self.name.blank? && first_name.present? && last_name.present? + self.name ||= first_name + ' ' + last_name + end + end - def set_defaults - self.name ||= self.user.name if self.user - self.email ||= self.user.email if self.user - self.picture ||= self.user.picture if self.user - if self.name.blank? && self.first_name.present? && self.last_name.present? - self.name ||= self.first_name + ' ' + self.last_name - end - end + # Queries - # Queries + def recent_donations(npo_id) + donations.valid.order('created_at').where(nonprofit_id: npo_id).take(10) + end - def recent_donations(npo_id) - self.donations.valid.order("created_at").where(nonprofit_id: npo_id).take(10) - end + # Attrs - # Attrs + def total_given_to(nonprofit) + donations.valid.where(nonprofit_id: nonprofit.id).pluck(:amount).sum + end - def total_given_to(nonprofit) - self.donations.valid.where(nonprofit_id: nonprofit.id).pluck(:amount).sum - end + def monthly_giving(nonprofit_id) + donations.where(nonprofit_id: nonprofit_id).map(&:amount).sum + end - def monthly_giving(nonprofit_id) - self.donations.where(nonprofit_id: nonprofit_id).map(&:amount).sum - end + def monthly_total_giving + donations.map(&:amount).sum + end - def monthly_total_giving - self.donations.map(&:amount).sum - end + def full_name + "#{first_name} #{last_name}" + end - def full_name - "#{self.first_name} #{self.last_name}" - end + def supporter_name + self.name.blank? ? 'A Supporter' : self.name + end - def supporter_name - self.name.blank? ? "A Supporter" : self.name - end + def get_profile_picture(size = :normal) + # Can be, in order of precedence: your uploaded photo, facebook picture, or + # default image + if user.picture + return user.get_picture(size) + else + return picture_url(size) + end - def get_profile_picture(size=:normal) - # Can be, in order of precedence: your uploaded photo, facebook picture, or - # default image - if self.user.picture - return self.user.get_picture(size) - else - return self.picture_url(size) - end - # Either does not want photo shown or has none uploaded. - return Image::DefaultProfileUrl - end + # Either does not want photo shown or has none uploaded. + Image::DefaultProfileUrl + end - def url - Rails.application.routes.url_helpers.profile_path(self) - end + def url + Rails.application.routes.url_helpers.profile_path(self) + end - def as_json(options = {}) - h = super(options) - h[:pic_tiny] = self.get_profile_picture :tiny - h[:url] = self.url - h - end + def as_json(options = {}) + h = super(options) + h[:pic_tiny] = get_profile_picture :tiny + h[:url] = url + h + end - # Cache setters - - def set_caches! - self.total_raised = self.donations.pluck(:amount).sum - self.total_recurring = self.recurring_donations.active.pluck(:amount).sum - self.save! - end + # Cache setters + def set_caches! + self.total_raised = donations.pluck(:amount).sum + self.total_recurring = recurring_donations.active.pluck(:amount).sum + save! + end end diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index 8833b076..ece20b80 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RecurringDonation < ApplicationRecord - - #TODO: + # TODO: # attr_accessible \ # :amount, # int (cents) # :active, # bool (whether this recurring donation should still be paid) @@ -19,10 +20,10 @@ class RecurringDonation < ApplicationRecord # :nonprofit_id, :nonprofit, # :supporter_id #used because things are messed up in the datamodel - scope :active, -> {where(active: true)} - scope :inactive, -> {where(active: [false,nil])} - scope :monthly, -> {where(time_unit: 'month', interval: 1)} - scope :annual, -> {where(time_unit: 'year', interval: 1)} + scope :active, -> { where(active: true) } + scope :inactive, -> { where(active: [false, nil]) } + scope :monthly, -> { where(time_unit: 'month', interval: 1) } + scope :annual, -> { where(time_unit: 'year', interval: 1) } belongs_to :donation belongs_to :nonprofit @@ -30,37 +31,30 @@ class RecurringDonation < ApplicationRecord has_one :card, through: :donation has_one :supporter, through: :donation - validates :paydate, numericality: {less_than: 29}, allow_blank: true + validates :paydate, numericality: { less_than: 29 }, allow_blank: true validates :donation_id, presence: true validates :nonprofit_id, presence: true validates :start_date, presence: true - validates :interval, presence: true, numericality: {greater_than: 0} - validates :time_unit, presence: true, inclusion: {in: Timespan::Units} + validates :interval, presence: true, numericality: { greater_than: 0 } + validates :time_unit, presence: true, inclusion: { in: Timespan::Units } validates_associated :donation def most_recent_charge - if (self.charges) - return self.charges.sort_by { |c| c.created_at }.last() - end + charges&.max_by(&:created_at) end def most_recent_paid_charge - if (self.charges) - return self.charges.find_all {|c| c.paid?}.sort_by { |c| c.created_at }.last() - end + charges&.find_all(&:paid?)&.max_by(&:created_at) end def total_given - if (self.charges) - return self.charges.find_all(&:paid?).sum(&:amount) - end - + return charges.find_all(&:paid?).sum(&:amount) if charges end # XXX let's make these monthly_totals a query # Or just push it into the front-end def self.monthly_total - self.all.map(&:monthly_total).sum + all.map(&:monthly_total).sum end def monthly_total @@ -68,8 +62,7 @@ class RecurringDonation < ApplicationRecord 'week' => 4, 'day' => 30, 'year' => 0.0833 - }[self.interval] || 1 - return self.donation.amount * multiple + }[interval] || 1 + donation.amount * multiple end - end diff --git a/app/models/refund.rb b/app/models/refund.rb index ef37db60..e315552b 100644 --- a/app/models/refund.rb +++ b/app/models/refund.rb @@ -1,27 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Refund < ApplicationRecord + Reasons = %i[duplicate fraudulent requested_by_customer].freeze - Reasons = [:duplicate, :fraudulent, :requested_by_customer] + # TODO: + # attr_accessible \ + # :amount, # int + # :comment, # text + # :reason, # str ('duplicate', 'fraudulent', or 'requested_by_customer') + # :stripe_refund_id, + # :disbursed, # boolean (whether this refund has been counted in a payout) + # :failure_message, # str (accessor for storing the Stripe error message) + # :user_id, :user, # user who made this refund + # :payment_id, :payment, # negative payment that records this refund + # :charge_id, :charge - # TODO: - # attr_accessible \ - # :amount, # int - # :comment, # text - # :reason, # str ('duplicate', 'fraudulent', or 'requested_by_customer') - # :stripe_refund_id, - # :disbursed, # boolean (whether this refund has been counted in a payout) - # :failure_message, # str (accessor for storing the Stripe error message) - # :user_id, :user, # user who made this refund - # :payment_id, :payment, # negative payment that records this refund - # :charge_id, :charge + attr_accessor :failure_message - attr_accessor :failure_message - - belongs_to :charge - belongs_to :payment - - scope :not_disbursed, ->{where(disbursed: [nil, false])} - scope :disbursed, ->{where(disbursed: [true])} + belongs_to :charge + belongs_to :payment + scope :not_disbursed, -> { where(disbursed: [nil, false]) } + scope :disbursed, -> { where(disbursed: [true]) } end - diff --git a/app/models/role.rb b/app/models/role.rb index 215e24af..3063a4f7 100644 --- a/app/models/role.rb +++ b/app/models/role.rb @@ -1,50 +1,58 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Role < ApplicationRecord + Names = [ + :super_admin, # global access + :super_associate, # global access to everything except bank acct info + :nonprofit_admin, # npo scoped access to everything + :nonprofit_associate, # npo scoped access to everything except bank acct info + :campaign_editor, # fundraising tools, without dashboard access + :event_editor # // + ].freeze - Names = [ - :super_admin, # global access - :super_associate, # global access to everything except bank acct info - :nonprofit_admin, # npo scoped access to everything - :nonprofit_associate, # npo scoped access to everything except bank acct info - :campaign_editor, # fundraising tools, without dashboard access - :event_editor # // - ] + # TODO: + # attr_accessible \ + # :name, + # :user_id, :user, + # :host, :host_id, :host_type # nil, "Nonprofit", "Campaign", "Event" - # TODO: - # attr_accessible \ - # :name, - # :user_id, :user, - # :host, :host_id, :host_type # nil, "Nonprofit", "Campaign", "Event" + belongs_to :user + belongs_to :host, polymorphic: true - belongs_to :user - belongs_to :host, polymorphic: true + scope :super_admins, -> { where(name: :super_admin) } + scope :super_associate, -> { where(name: :super_associate) } + scope :nonprofit_admins, -> { where(name: :nonprofit_admin) } + scope :nonprofit_personnel, -> { where(name: %i[nonprofit_associate nonprofit_admin]) } + scope :campaign_editors, -> { where(name: :campaign_editor) } + scope :event_editors, -> { where(name: :event_editor) } - scope :super_admins, -> {where(name: :super_admin)} - scope :super_associate, -> {where(name: :super_associate)} - scope :nonprofit_admins, -> {where(name: :nonprofit_admin)} - scope :nonprofit_personnel, -> {where(name: [:nonprofit_associate, :nonprofit_admin])} - scope :campaign_editors, -> {where(name: :campaign_editor)} - scope :event_editors, -> {where(name: :event_editor)} - - validates :user, presence: true - validates :name, inclusion: {in: Names} - validates :host, presence: true, unless: [:super_admin?, :super_associate?] - - def name; super.to_sym; end - def super_admin?; name == :super_admin; end - def super_associate?; name == :super_associate; end - - def self.create_for_nonprofit(role_name, email, nonprofit) - user = User.find_or_create_with_email(email) - role = Role.create(user: user, name: role_name, host: nonprofit) - return role unless role.valid? - if user.confirmed? - NonprofitAdminMailer.delay.existing_invite(role) - else - NonprofitAdminMailer.delay.new_invite(role, user.make_confirmation_token!) - end - return role - end + validates :user, presence: true + validates :name, inclusion: { in: Names } + validates :host, presence: true, unless: %i[super_admin? super_associate?] + def name + super.to_sym end + def super_admin? + name == :super_admin +end + + def super_associate? + name == :super_associate +end + + def self.create_for_nonprofit(role_name, email, nonprofit) + user = User.find_or_create_with_email(email) + role = Role.create(user: user, name: role_name, host: nonprofit) + return role unless role.valid? + + if user.confirmed? + NonprofitAdminMailer.delay.existing_invite(role) + else + NonprofitAdminMailer.delay.new_invite(role, user.make_confirmation_token!) + end + role + end +end diff --git a/app/models/source_token.rb b/app/models/source_token.rb index 159d682f..7e9ba7dc 100644 --- a/app/models/source_token.rb +++ b/app/models/source_token.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SourceToken < ApplicationRecord self.primary_key = :token # attr_accessible :expiration, :token, :max_uses, :total_uses - belongs_to :tokenizable, :polymorphic => true + belongs_to :tokenizable, polymorphic: true belongs_to :event end diff --git a/app/models/supporter.rb b/app/models/supporter.rb index cdb087a2..cfaf71de 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Supporter < ApplicationRecord - - #TODO + # TODO # attr_accessible \ # :search_vectors, # :profile_id, :profile, @@ -49,10 +50,10 @@ class Supporter < ApplicationRecord has_many :tag_masters, through: :tag_joins has_many :custom_field_joins, dependent: :destroy has_many :custom_field_masters, through: :custom_field_joins - belongs_to :merged_into, class_name: 'Supporter', :foreign_key => 'merged_into' + belongs_to :merged_into, class_name: 'Supporter', foreign_key: 'merged_into' - validates :nonprofit, :presence => true - scope :not_deleted, -> {where(deleted: false)} + validates :nonprofit, presence: true + scope :not_deleted, -> { where(deleted: false) } geocoded_by :full_address reverse_geocoded_by :latitude, :longitude do |obj, results| @@ -66,22 +67,21 @@ class Supporter < ApplicationRecord end end - def profile_picture size=:normal - return unless self.profile - self.profile.get_profile_picture(size) - end + def profile_picture(size = :normal) + return unless profile + profile.get_profile_picture(size) + end def as_json(options = {}) h = super(options) - h[:pic_tiny] = self.profile_picture(:tiny) - h[:pic_normal] = self.profile_picture(:normal) - h[:url] = self.profile && Rails.application.routes.url_helpers.profile_path(self.profile) - return h + h[:pic_tiny] = profile_picture(:tiny) + h[:pic_normal] = profile_picture(:normal) + h[:url] = profile && Rails.application.routes.url_helpers.profile_path(profile) + h end def full_address - Format::Address.full_address(self.address, self.city, self.state_code) + Format::Address.full_address(address, city, state_code) end - end diff --git a/app/models/supporter_email.rb b/app/models/supporter_email.rb index 3cd856f9..13864dbd 100644 --- a/app/models/supporter_email.rb +++ b/app/models/supporter_email.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SupporterEmail < ApplicationRecord - # TODO - # attr_accessible \ - # :to, - # :from, - # :subject, - # :body, - # :recipient_count, - # :supporter_id, :supporter, - # :nonprofit_id, - # :gmail_thread_id + # TODO + # attr_accessible \ + # :to, + # :from, + # :subject, + # :body, + # :recipient_count, + # :supporter_id, :supporter, + # :nonprofit_id, + # :gmail_thread_id - belongs_to :supporter - validates_presence_of :nonprofit_id - has_many :activities, as: :attachment, dependent: :destroy + belongs_to :supporter + validates_presence_of :nonprofit_id + has_many :activities, as: :attachment, dependent: :destroy end diff --git a/app/models/supporter_note.rb b/app/models/supporter_note.rb index 8701dd2b..3c827690 100644 --- a/app/models/supporter_note.rb +++ b/app/models/supporter_note.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SupporterNote < ApplicationRecord + # TODO + # attr_accessible \ + # :content, + # :supporter_id, :supporter - #TODO - # attr_accessible \ - # :content, - # :supporter_id, :supporter + belongs_to :supporter + has_many :activities, as: :attachment, dependent: :destroy - belongs_to :supporter - has_many :activities, as: :attachment, dependent: :destroy - - validates :content, length: {minimum: 1} - validates :supporter_id, presence: true + validates :content, length: { minimum: 1 } + validates :supporter_id, presence: true end - diff --git a/app/models/tag_join.rb b/app/models/tag_join.rb index 45ee10c5..d6ca78ca 100644 --- a/app/models/tag_join.rb +++ b/app/models/tag_join.rb @@ -1,25 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TagJoin < ApplicationRecord + # TODO + # attr_accessible \ + # :supporter, :supporter_id, + # :tag_master, :tag_master_id - #TODO - # attr_accessible \ - # :supporter, :supporter_id, - # :tag_master, :tag_master_id + validates :tag_master, presence: true - validates :tag_master, presence: true - - belongs_to :tag_master - belongs_to :supporter - - def name; self.tag_master.name; end - - def self.create_with_name(nonprofit, h) - tm = nonprofit.tag_masters.find_by_name(h['name']) - if tm.nil? - tm = nonprofit.tag_masters.create(name: h['name']) - end - self.create tag_master: tm, supporter_id: h['supporter_id'] - end + belongs_to :tag_master + belongs_to :supporter + def name + tag_master.name end + def self.create_with_name(nonprofit, h) + tm = nonprofit.tag_masters.find_by_name(h['name']) + tm = nonprofit.tag_masters.create(name: h['name']) if tm.nil? + create tag_master: tm, supporter_id: h['supporter_id'] + end +end diff --git a/app/models/tag_master.rb b/app/models/tag_master.rb index 9bac71ce..b613275d 100644 --- a/app/models/tag_master.rb +++ b/app/models/tag_master.rb @@ -1,26 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TagMaster < ApplicationRecord + # TODO: + # attr_accessible \ + # :nonprofit, :nonprofit_id, + # :name, + # :deleted, + # :created_at - #TODO: - # attr_accessible \ - # :nonprofit, :nonprofit_id, - # :name, - # :deleted, - # :created_at + validates :name, presence: true + validate :no_dupes, on: :create - validates :name, presence: true - validate :no_dupes, on: :create + belongs_to :nonprofit + has_many :tag_joins, dependent: :destroy + has_one :email_list - belongs_to :nonprofit - has_many :tag_joins, dependent: :destroy - has_one :email_list + scope :not_deleted, -> { where(deleted: [nil, false]) } - scope :not_deleted, ->{where(deleted: [nil,false])} - - def no_dupes - return self if nonprofit.nil? - errors.add(:base, "Duplicate tag") if nonprofit.tag_masters.not_deleted.where(name: name).any? - end + def no_dupes + return self if nonprofit.nil? + errors.add(:base, 'Duplicate tag') if nonprofit.tag_masters.not_deleted.where(name: name).any? + end end - diff --git a/app/models/ticket.rb b/app/models/ticket.rb index ee5ad133..e8292b7d 100644 --- a/app/models/ticket.rb +++ b/app/models/ticket.rb @@ -1,21 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Ticket < ApplicationRecord - # attr_accessible :note, :event_discount, :event_discount_id belongs_to :event_discount - belongs_to :supporter - belongs_to :profile - belongs_to :ticket_level - belongs_to :event - belongs_to :charge - belongs_to :card - belongs_to :payment - belongs_to :source_token - has_one :nonprofit, through: :event - has_many :activities, as: :attachment, dependent: :destroy + belongs_to :supporter + belongs_to :profile + belongs_to :ticket_level + belongs_to :event + belongs_to :charge + belongs_to :card + belongs_to :payment + belongs_to :source_token + has_one :nonprofit, through: :event + has_many :activities, as: :attachment, dependent: :destroy - def related_tickets - payment.tickets.where('id != ?', self.id) - end + def related_tickets + payment.tickets.where('id != ?', id) + end end diff --git a/app/models/ticket_level.rb b/app/models/ticket_level.rb index d9d7942d..dd0130d5 100644 --- a/app/models/ticket_level.rb +++ b/app/models/ticket_level.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketLevel < ApplicationRecord - - #TODO + # TODO # attr_accessible \ # :amount, #integer # :amount_dollars, #accessor, string @@ -19,14 +20,13 @@ class TicketLevel < ApplicationRecord has_many :tickets belongs_to :event - validates :name, :presence => true - validates :event_id, :presence => true + validates :name, presence: true + validates :event_id, presence: true - scope :not_deleted, ->{where(deleted: [false,nil])} + scope :not_deleted, -> { where(deleted: [false, nil]) } before_validation do - self.amount = Format::Currency.dollars_to_cents(self.amount_dollars) if self.amount_dollars.present? - self.amount = 0 if self.amount.nil? + self.amount = Format::Currency.dollars_to_cents(amount_dollars) if amount_dollars.present? + self.amount = 0 if amount.nil? end - end diff --git a/app/models/tracking.rb b/app/models/tracking.rb index 0903b3e3..afb3cf75 100644 --- a/app/models/tracking.rb +++ b/app/models/tracking.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Tracking < ApplicationRecord # attr_accessible :utm_campaign, :utm_content, :utm_medium, :utm_source diff --git a/app/models/user.rb b/app/models/user.rb index 31b0aba1..7a9af591 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,110 +1,110 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class User < ApplicationRecord + # TODO: + # attr_accessible \ + # :email, # str: balidated with Devise + # :password, # str: hashed with bcrypt + # :phone, # str + # :location, + # :city, + # :state_code, + # :password_confirmation, # accessor: used on registration + # :remember_me, # bool: don't sign user out for a while + # :provider, # str: OAuth provider + # :uid, # str: OAuth user ID + # :pending_password, # bool: User registered with oauth and did not set a password + # :name, # str: created with oauth + # :auto_generated, # bool: flag whether a password was auto-generated for this account + # :referer, # str: ID of the user who referred this account + # :latitude, + # :longitude, + # :reset_password_token, + # :reset_password_sent_at, + # :picture, # str: url for fb or twitter pic + # :current_password, # accessor: for updating pass + # :profile_attributes, + # :phone - #TODO: - # attr_accessible \ - # :email, # str: balidated with Devise - # :password, # str: hashed with bcrypt - # :phone, # str - # :location, - # :city, - # :state_code, - # :password_confirmation, # accessor: used on registration - # :remember_me, # bool: don't sign user out for a while - # :provider, # str: OAuth provider - # :uid, # str: OAuth user ID - # :pending_password, # bool: User registered with oauth and did not set a password - # :name, # str: created with oauth - # :auto_generated, # bool: flag whether a password was auto-generated for this account - # :referer, # str: ID of the user who referred this account - # :latitude, - # :longitude, - # :reset_password_token, - # :reset_password_sent_at, - # :picture, # str: url for fb or twitter pic - # :current_password, # accessor: for updating pass - # :profile_attributes, - # :phone + geocoded_by :location - geocoded_by :location + devise :async, :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable - devise :async, :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable + attr_accessor :offsite_donation_id, :current_password - attr_accessor :offsite_donation_id, :current_password + validates :email, + presence: true, + uniqueness: { case_sensitive: false }, + format: { with: Email::Regex } - validates :email, - presence: true, - uniqueness: {case_sensitive: false}, - format: {with: Email::Regex} - - has_many :donations, through: :profile - has_many :roles, dependent: :destroy - has_one :profile, dependent: :destroy - has_many :imports + has_many :donations, through: :profile + has_many :roles, dependent: :destroy + has_one :profile, dependent: :destroy + has_many :imports has_many :email_settings - accepts_nested_attributes_for :profile + accepts_nested_attributes_for :profile - before_validation(on: :create) do - self.password = Devise.friendly_token.first(8) if self.auto_generated - self.build_profile if self.profile.nil? - self - end + before_validation(on: :create) do + self.password = Devise.friendly_token.first(8) if auto_generated + build_profile if profile.nil? + self + end # This creates the user in the normal way, but also sends the devise email confirmation email, which we don't want to send to np admins or anyone else def self.register_donor!(params) u = User.create!(params) u.send_confirmation_instructions - return u - end + u + end def self.find_or_create_with_email(em) - user = self.where("lower(email) = ?", em.downcase).first + user = where('lower(email) = ?', em.downcase).first return user if user.present? + User.create!(email: em, auto_generated: true) end - def profile_picture(size) - self.profile.picture_url(size) - end + def profile_picture(size) + profile.picture_url(size) + end + # Required by Devise for Omniauth + # https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview + def self.new_with_session(params, session) + super.tap do |user| + if data = session['devise.facebook_data'] && session['devise.facebook_data']['extra']['raw_info'] + user.email = data['email'] if user.email.blank? + end + end + end - # Required by Devise for Omniauth - # https://github.com/plataformatec/devise/wiki/OmniAuth:-Overview - def self.new_with_session(params, session) - super.tap do |user| - if data = session['devise.facebook_data'] && session['devise.facebook_data']['extra']['raw_info'] - user.email = data['email'] if user.email.blank? - end - end - end + # Don't require confirmation for new users -- they can still donate without confirmation + # https://github.com/plataformatec/devise/wiki/How-To:-Override-confirmations-so-users-can-pick-their-own-passwords-as-part-of-confirmation-activation + def confirmation_required? + false + end - # Don't require confirmation for new users -- they can still donate without confirmation - # https://github.com/plataformatec/devise/wiki/How-To:-Override-confirmations-so-users-can-pick-their-own-passwords-as-part-of-confirmation-activation - def confirmation_required? - false - end - - def as_json(options={}) - h = super(options) - h[:unconfirmed_email] = self.unconfirmed_email - h[:confirmed] = self.confirmed? - h[:profile] = self.profile.as_json - h - end + def as_json(options = {}) + h = super(options) + h[:unconfirmed_email] = unconfirmed_email + h[:confirmed] = confirmed? + h[:profile] = profile.as_json + h + end # This is useful for manually generating a Devise user confirmation token so that we can get the confirmation URL with the correct token from anywhere def make_confirmation_token! raw, db = Devise.token_generator.generate(User, :confirmation_token) self.confirmation_token = db self.confirmation_sent_at = Time.now - self.save! - return raw + save! + raw end - def geocode! - #self.geocode - #self.save - end - + def geocode! + # self.geocode + # self.save + end end diff --git a/app/uploaders/article_background_uploader.rb b/app/uploaders/article_background_uploader.rb index 3fbaa342..9ac0229f 100644 --- a/app/uploaders/article_background_uploader.rb +++ b/app/uploaders/article_background_uploader.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class ArticleBackgroundUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -20,7 +21,7 @@ class ArticleBackgroundUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultNonprofitUrl + Image::DefaultNonprofitUrl end # Process files as they are uploaded: @@ -32,14 +33,13 @@ class ArticleBackgroundUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :large do - process :resize_to_fill => [600, 400] + process resize_to_fill: [600, 400] end - # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -51,5 +51,4 @@ class ArticleBackgroundUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/article_uploader.rb b/app/uploaders/article_uploader.rb index ac4b0d86..906c96ad 100644 --- a/app/uploaders/article_uploader.rb +++ b/app/uploaders/article_uploader.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class ArticleUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -20,7 +21,7 @@ class ArticleUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultNonprofitUrl + Image::DefaultNonprofitUrl end # Process files as they are uploaded: @@ -32,13 +33,13 @@ class ArticleUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :thumb do - process :resize_to_fill => [200, 200] + process resize_to_fill: [200, 200] end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -50,5 +51,4 @@ class ArticleUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/campaign_background_image_uploader.rb b/app/uploaders/campaign_background_image_uploader.rb index 57b6c6cb..da31b91c 100644 --- a/app/uploaders/campaign_background_image_uploader.rb +++ b/app/uploaders/campaign_background_image_uploader.rb @@ -1,26 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class CampaignBackgroundImageUploader < CarrierWave::Uploader::Base + include CarrierWave::MiniMagick - include CarrierWave::MiniMagick - + def store_dir + "uploads/campaigns/#{mounted_as}/#{model.id}" + end - def store_dir - "uploads/campaigns/#{mounted_as}/#{model.id}" - end + # Create different versions of your uploaded files: + version :normal do + process resize_to_fill: [1000, 600] + end - # Create different versions of your uploaded files: - version :normal do - process :resize_to_fill => [1000, 600] - end - - def extension_white_list - %w(jpg jpeg png) - end - - def cache_dir - "#{Rails.root}/tmp/uploads" - end + def extension_white_list + %w[jpg jpeg png] + end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/uploaders/campaign_banner_image_uploader.rb b/app/uploaders/campaign_banner_image_uploader.rb index 1c4010d7..09fe0e46 100644 --- a/app/uploaders/campaign_banner_image_uploader.rb +++ b/app/uploaders/campaign_banner_image_uploader.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignBannerImageUploader < CarrierWave::Uploader::Base - include CarrierWave::MiniMagick + include CarrierWave::MiniMagick - def store_dir - "uploads/campaigns/#{mounted_as}/#{model.id}" - end + def store_dir + "uploads/campaigns/#{mounted_as}/#{model.id}" + end - def extension_white_list - %w(jpg jpeg png) - end + def extension_white_list + %w[jpg jpeg png] + end - def cache_dir - "#{Rails.root}/tmp/uploads" - end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/uploaders/campaign_main_image_uploader.rb b/app/uploaders/campaign_main_image_uploader.rb index bcf66cf1..a8def8e4 100644 --- a/app/uploaders/campaign_main_image_uploader.rb +++ b/app/uploaders/campaign_main_image_uploader.rb @@ -1,56 +1,56 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignMainImageUploader < CarrierWave::Uploader::Base + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + include CarrierWave::MiniMagick - # Include RMagick or MiniMagick support: - # include CarrierWave::RMagick - include CarrierWave::MiniMagick + # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: + # include Sprockets::Helpers::RailsHelper + # include Sprockets::Helpers::IsolatedHelper - # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: - # include Sprockets::Helpers::RailsHelper - # include Sprockets::Helpers::IsolatedHelper + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "uploads/campaigns/#{mounted_as}/#{model.id}" + end - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/campaigns/#{mounted_as}/#{model.id}" - end + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + # For Rails 3.1+ asset pipeline compatibility: + Image::DefaultProfileUrl + end - # Provide a default URL as a default if there hasn't been a file uploaded: - def default_url - # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl - end + # Process files as they are uploaded: + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end - # Process files as they are uploaded: - # process :scale => [200, 300] - # - # def scale(width, height) - # # do something - # end + # Create different versions of your uploaded files: + version :normal do + process resize_to_fill: [524, 360] + end - # Create different versions of your uploaded files: - version :normal do - process :resize_to_fill => [524, 360] - end + version :thumb do + process resize_to_fill: [180, 150] + end - version :thumb do - process :resize_to_fill => [180, 150] - end + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_white_list + %w[jpg jpeg png] + end - # Add a white list of extensions which are allowed to be uploaded. - # For images you might use something like this: - def extension_white_list - %w(jpg jpeg png) - end - - # Override the filename of the uploaded files: - # Avoid using model.id or version_name here, see uploader/store.rb for details. - # def filename - # "something.jpg" if original_filename - # end - - def cache_dir - "#{Rails.root}/tmp/uploads" - end + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/uploaders/event_background_image_uploader.rb b/app/uploaders/event_background_image_uploader.rb index 02a304d2..123517b2 100644 --- a/app/uploaders/event_background_image_uploader.rb +++ b/app/uploaders/event_background_image_uploader.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventBackgroundImageUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -18,7 +19,7 @@ class EventBackgroundImageUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultCampaignUrl + Image::DefaultCampaignUrl end # Process files as they are uploaded: @@ -30,13 +31,13 @@ class EventBackgroundImageUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :normal do - process :resize_to_fill => [1000, 600] + process resize_to_fill: [1000, 600] end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -48,5 +49,4 @@ class EventBackgroundImageUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/event_main_image_uploader.rb b/app/uploaders/event_main_image_uploader.rb index 6afc1216..e8b8adb1 100644 --- a/app/uploaders/event_main_image_uploader.rb +++ b/app/uploaders/event_main_image_uploader.rb @@ -1,56 +1,56 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventMainImageUploader < CarrierWave::Uploader::Base + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + include CarrierWave::MiniMagick - # Include RMagick or MiniMagick support: - # include CarrierWave::RMagick - include CarrierWave::MiniMagick + # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: + # include Sprockets::Helpers::RailsHelper + # include Sprockets::Helpers::IsolatedHelper - # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: - # include Sprockets::Helpers::RailsHelper - # include Sprockets::Helpers::IsolatedHelper + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "uploads/events/#{mounted_as}/#{model.id}" + end - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/events/#{mounted_as}/#{model.id}" - end + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + # For Rails 3.1+ asset pipeline compatibility: + Image::DefaultProfileUrl + end - # Provide a default URL as a default if there hasn't been a file uploaded: - def default_url - # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl - end + # Process files as they are uploaded: + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end - # Process files as they are uploaded: - # process :scale => [200, 300] - # - # def scale(width, height) - # # do something - # end + # Create different versions of your uploaded files: + version :normal do + process resize_to_fill: [400, 400] + end - # Create different versions of your uploaded files: - version :normal do - process :resize_to_fill => [400, 400] - end + version :thumb do + process resize_to_fill: [100, 100] + end - version :thumb do - process :resize_to_fill => [100,100] - end + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_white_list + %w[jpg jpeg png] + end - # Add a white list of extensions which are allowed to be uploaded. - # For images you might use something like this: - def extension_white_list - %w(jpg jpeg png) - end - - # Override the filename of the uploaded files: - # Avoid using model.id or version_name here, see uploader/store.rb for details. - # def filename - # "something.jpg" if original_filename - # end - - def cache_dir - "#{Rails.root}/tmp/uploads" - end + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/uploaders/image_attachment_uploader.rb b/app/uploaders/image_attachment_uploader.rb index 01b644f6..a5e6d304 100644 --- a/app/uploaders/image_attachment_uploader.rb +++ b/app/uploaders/image_attachment_uploader.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class ImageAttachmentUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -20,7 +21,7 @@ class ImageAttachmentUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl + Image::DefaultProfileUrl end # Process files as they are uploaded: @@ -32,23 +33,23 @@ class ImageAttachmentUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :large do - process :resize_to_fill => [600, 400] + process resize_to_fill: [600, 400] end version :medium do - process :resize_to_fill => [400, 266] + process resize_to_fill: [400, 266] end version :small do - process :resize_to_fill => [400, 266] + process resize_to_fill: [400, 266] end # slightly smaller than the normal thumb version :thumb_explore do - process :resize_to_fill => [200, 133] + process resize_to_fill: [200, 133] end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -60,5 +61,4 @@ class ImageAttachmentUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/nonprofit_background_uploader.rb b/app/uploaders/nonprofit_background_uploader.rb index b46f6284..e6134f68 100644 --- a/app/uploaders/nonprofit_background_uploader.rb +++ b/app/uploaders/nonprofit_background_uploader.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class NonprofitBackgroundUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -21,7 +22,7 @@ class NonprofitBackgroundUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultNonprofitUrl + Image::DefaultNonprofitUrl end # Process files as they are uploaded: @@ -33,13 +34,13 @@ class NonprofitBackgroundUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :normal do - process :resize_to_fill => [1000, 600] + process resize_to_fill: [1000, 600] end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -51,5 +52,4 @@ class NonprofitBackgroundUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/nonprofit_logo_uploader.rb b/app/uploaders/nonprofit_logo_uploader.rb index c068b73c..55dd242e 100644 --- a/app/uploaders/nonprofit_logo_uploader.rb +++ b/app/uploaders/nonprofit_logo_uploader.rb @@ -1,51 +1,51 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class NonprofitLogoUploader < CarrierWave::Uploader::Base + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + include CarrierWave::MiniMagick - # Include RMagick or MiniMagick support: - # include CarrierWave::RMagick - include CarrierWave::MiniMagick + # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: + # include Sprockets::Helpers::RailsHelper + # include Sprockets::Helpers::IsolatedHelper - # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: - # include Sprockets::Helpers::RailsHelper - # include Sprockets::Helpers::IsolatedHelper + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "uploads/npo/#{mounted_as}/#{model.id}" + end - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/npo/#{mounted_as}/#{model.id}" - end + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + # For Rails 3.1+ asset pipeline compatibility: + Image::DefaultProfileUrl + end - # Provide a default URL as a default if there hasn't been a file uploaded: - def default_url - # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl - end + # Create different versions of your uploaded files: + version :large do + process resize_to_fit: [180, 180] + end + version :normal do + process resize_to_fit: [100, 100] + end + version :small do + process resize_to_fit: [30, 30] + end - # Create different versions of your uploaded files: - version :large do - process :resize_to_fit => [180, 180] - end - version :normal do - process :resize_to_fit => [100, 100] - end - version :small do - process :resize_to_fit => [30, 30] - end + def extension_white_list + %w[jpg jpeg png gif] + end - def extension_white_list - %w(jpg jpeg png gif) - end - - # Override the filename of the uploaded files: - # Avoid using model.id or version_name here, see uploader/store.rb for details. - # def filename - # "something.jpg" if original_filename - # end - - def cache_dir - "#{Rails.root}/tmp/uploads" - end + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/uploaders/nonprofit_uploader.rb b/app/uploaders/nonprofit_uploader.rb index fe37a30c..334d3df6 100755 --- a/app/uploaders/nonprofit_uploader.rb +++ b/app/uploaders/nonprofit_uploader.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class NonprofitUploader < CarrierWave::Uploader::Base - # Include RMagick or MiniMagick support: # include CarrierWave::RMagick include CarrierWave::MiniMagick @@ -20,7 +21,7 @@ class NonprofitUploader < CarrierWave::Uploader::Base # Provide a default URL as a default if there hasn't been a file uploaded: def default_url # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl + Image::DefaultProfileUrl end # Process files as they are uploaded: @@ -32,20 +33,20 @@ class NonprofitUploader < CarrierWave::Uploader::Base # Create different versions of your uploaded files: version :nonprofit_carousel do - process :resize_to_fill => [590, 338] + process resize_to_fill: [590, 338] end version :thumb do - process :resize_to_fill => [188, 120] + process resize_to_fill: [188, 120] end # slightly smaller than the normal thumb version :thumb_explore do - process :resize_to_fill => [100, 100] + process resize_to_fill: [100, 100] end # Add a white list of extensions which are allowed to be uploaded. # For images you might use something like this: def extension_white_list - %w(jpg jpeg png) + %w[jpg jpeg png] end # Override the filename of the uploaded files: @@ -57,5 +58,4 @@ class NonprofitUploader < CarrierWave::Uploader::Base def cache_dir "#{Rails.root}/tmp/uploads" end - end diff --git a/app/uploaders/profile_uploader.rb b/app/uploaders/profile_uploader.rb index ec911fd4..1abd9e4c 100644 --- a/app/uploaders/profile_uploader.rb +++ b/app/uploaders/profile_uploader.rb @@ -1,60 +1,60 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # encoding: utf-8 class ProfileUploader < CarrierWave::Uploader::Base + # Include RMagick or MiniMagick support: + # include CarrierWave::RMagick + include CarrierWave::MiniMagick - # Include RMagick or MiniMagick support: - # include CarrierWave::RMagick - include CarrierWave::MiniMagick + # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: + # include Sprockets::Helpers::RailsHelper + # include Sprockets::Helpers::IsolatedHelper - # Include the Sprockets helpers for Rails 3.1+ asset pipeline compatibility: - # include Sprockets::Helpers::RailsHelper - # include Sprockets::Helpers::IsolatedHelper + # Override the directory where uploaded files will be stored. + # This is a sensible default for uploaders that are meant to be mounted: + def store_dir + "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" + end - # Override the directory where uploaded files will be stored. - # This is a sensible default for uploaders that are meant to be mounted: - def store_dir - "uploads/#{model.class.to_s.underscore}/#{mounted_as}/#{model.id}" - end + # Provide a default URL as a default if there hasn't been a file uploaded: + def default_url + # For Rails 3.1+ asset pipeline compatibility: + Image::DefaultProfileUrl + end - # Provide a default URL as a default if there hasn't been a file uploaded: - def default_url - # For Rails 3.1+ asset pipeline compatibility: - return Image::DefaultProfileUrl - end + # Process files as they are uploaded: + # process :scale => [200, 300] + # + # def scale(width, height) + # # do something + # end - # Process files as they are uploaded: - # process :scale => [200, 300] - # - # def scale(width, height) - # # do something - # end + # Create different versions of your uploaded files: + version :normal do + process resize_to_fill: [150, 150] + end + version :medium do + process resize_to_fill: [100, 100] + end + version :tiny do + process resize_to_fill: [50, 50] + end - # Create different versions of your uploaded files: - version :normal do - process :resize_to_fill => [150, 150] - end - version :medium do - process :resize_to_fill => [100, 100] - end - version :tiny do - process :resize_to_fill => [50, 50] - end + # Add a white list of extensions which are allowed to be uploaded. + # For images you might use something like this: + def extension_white_list + %w[jpg jpeg png] + end - # Add a white list of extensions which are allowed to be uploaded. - # For images you might use something like this: - def extension_white_list - %w(jpg jpeg png) - end - - # Override the filename of the uploaded files: - # Avoid using model.id or version_name here, see uploader/store.rb for details. - # def filename - # "something.jpg" if original_filename - # end - - def cache_dir - "#{Rails.root}/tmp/uploads" - end + # Override the filename of the uploaded files: + # Avoid using model.id or version_name here, see uploader/store.rb for details. + # def filename + # "something.jpg" if original_filename + # end + def cache_dir + "#{Rails.root}/tmp/uploads" + end end diff --git a/app/views/campaigns/index.rabl b/app/views/campaigns/index.rabl index fe3366a8..d180c586 100644 --- a/app/views/campaigns/index.rabl +++ b/app/views/campaigns/index.rabl @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @campaigns => :data do - collection @campaigns, object_root: false - attributes :name, :total_raised, :goal_amount, :url, :id + collection @campaigns, object_root: false + attributes :name, :total_raised, :goal_amount, :url, :id end - diff --git a/app/views/events/index.rabl b/app/views/events/index.rabl index f449fe4b..ba1da37a 100644 --- a/app/views/events/index.rabl +++ b/app/views/events/index.rabl @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later child @events => :data do - collection @events, object_root: false - attributes :name, :date, :url, :id + collection @events, object_root: false + attributes :name, :date, :url, :id end diff --git a/app/views/maps/all_npo_supporters.rabl b/app/views/maps/all_npo_supporters.rabl index cee568f3..9dd4bbfc 100644 --- a/app/views/maps/all_npo_supporters.rabl +++ b/app/views/maps/all_npo_supporters.rabl @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id + collection @map_data, object_root: false + attributes :name, :latitude, :longitude, :id end diff --git a/app/views/maps/all_npos.rabl b/app/views/maps/all_npos.rabl index d511b8d0..2b04899e 100644 --- a/app/views/maps/all_npos.rabl +++ b/app/views/maps/all_npos.rabl @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone, :website + collection @map_data, object_root: false + attributes :name, :latitude, :longitude, :id, :email, :phone, :website end diff --git a/app/views/maps/all_supporters.rabl b/app/views/maps/all_supporters.rabl index 970c8663..0d64c6b1 100644 --- a/app/views/maps/all_supporters.rabl +++ b/app/views/maps/all_supporters.rabl @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone + collection @map_data, object_root: false + attributes :name, :latitude, :longitude, :id, :email, :phone end - diff --git a/app/views/maps/specific_npo_supporters.rabl b/app/views/maps/specific_npo_supporters.rabl index 25a94572..2d0ff097 100644 --- a/app/views/maps/specific_npo_supporters.rabl +++ b/app/views/maps/specific_npo_supporters.rabl @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone, :address, :city, :state_code, :total_raised + collection @map_data, object_root: false + attributes :name, :latitude, :longitude, :id, :email, :phone, :address, :city, :state_code, :total_raised end diff --git a/app/views/nonprofits/custom_field_joins/index.rabl b/app/views/nonprofits/custom_field_joins/index.rabl index cdae3879..92978134 100644 --- a/app/views/nonprofits/custom_field_joins/index.rabl +++ b/app/views/nonprofits/custom_field_joins/index.rabl @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @custom_field_joins => :data do - collection @custom_field_joins, object_root: false - attributes :name, :created_at, :id, :value + collection @custom_field_joins, object_root: false + attributes :name, :created_at, :id, :value end diff --git a/app/views/nonprofits/custom_field_masters/index.rabl b/app/views/nonprofits/custom_field_masters/index.rabl index d72b90f2..b211fd1e 100644 --- a/app/views/nonprofits/custom_field_masters/index.rabl +++ b/app/views/nonprofits/custom_field_masters/index.rabl @@ -1,9 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @custom_field_masters => :data do - collection @custom_field_masters, object_root: false - attributes :name, :id, :created_at + collection @custom_field_masters, object_root: false + attributes :name, :id, :created_at end - - diff --git a/app/views/nonprofits/payments/show.rabl b/app/views/nonprofits/payments/show.rabl index bd31ac39..c63e7e72 100644 --- a/app/views/nonprofits/payments/show.rabl +++ b/app/views/nonprofits/payments/show.rabl @@ -1,36 +1,36 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object @payment => :data attributes :gross_amount, :towards, :net_amount, :fee_total, :id, :date, :refund_total, :kind node(:consider_donation_anonymous) do |p| - d_anonymous = p.donation.nil? ? false : p.donation.anonymous + d_anonymous = p.donation.nil? ? false : p.donation.anonymous - !!d_anonymous || !!p.supporter.anonymous + !!d_anonymous || !!p.supporter.anonymous end - child :charge do - attributes :created_at, :id, :status + attributes :created_at, :id, :status end child :donation, object_root: false do - attributes :designation, :dedication, :origin_url, :id, :comment - + attributes :designation, :dedication, :origin_url, :id, :comment child :campaign, object_root: false do - attributes :name, :url, :id + attributes :name, :url, :id end - node(:campaign_gift){|d| {name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil}} + node(:campaign_gift) { |d| { name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil } } child :event, object_root: false do attributes :name, :url, :id end - child :recurring_donation, object_root: false do - attributes :interval, :time_unit, :created_at - end + child :recurring_donation, object_root: false do + attributes :interval, :time_unit, :created_at + end end child :dispute, object_root: false do @@ -38,37 +38,35 @@ child :dispute, object_root: false do end child :refund do - attributes :reason, :comment, :disbursed + attributes :reason, :comment, :disbursed end child :offsite_payment do - attributes :check_number, :kind + attributes :check_number, :kind end - node(:ticket) do |payment| event = payment&.tickets&.last&.event h = { - event: {name: event&.name, url: event&.url, id: event&.id}, - levels: payment.tickets.map{|t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)"}.join(", "), - discount: payment.tickets.map{|t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil}.compact.join(", ") + event: { name: event&.name, url: event&.url, id: event&.id }, + levels: payment.tickets.map { |t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)" }.join(', '), + discount: payment.tickets.map { |t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil }.compact.join(', ') } event ? h : nil end child :tickets, object_root: false do - attributes :id + attributes :id - child :ticket_level do - attributes :name - end + child :ticket_level do + attributes :name + end end child :supporter do - attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country - + attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country end child :nonprofit do - attributes :id + attributes :id end diff --git a/app/views/nonprofits/recurring_donations/show.rabl b/app/views/nonprofits/recurring_donations/show.rabl index 35a7b508..f8040f1a 100644 --- a/app/views/nonprofits/recurring_donations/show.rabl +++ b/app/views/nonprofits/recurring_donations/show.rabl @@ -1,15 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object @recurring_donation => :data attributes :id, :total_given, :supporter_id, :interval, :time_unit, :designation, :anonymous, :start_date, :end_date, :created_at, :paydate, :edit_token child :donation do - attributes :amount, :designation + attributes :amount, :designation end child :supporter do - attributes :name, :email, :id, :anonymous + attributes :name, :email, :id, :anonymous end child :card do - attributes :name + attributes :name end diff --git a/app/views/nonprofits/refunds/index.rabl b/app/views/nonprofits/refunds/index.rabl index afe5ee75..a837a07b 100644 --- a/app/views/nonprofits/refunds/index.rabl +++ b/app/views/nonprofits/refunds/index.rabl @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later object false child @refunds => :data do - collection @refunds, object_root: false - attributes :id, :amount, :created_at, :reason, :comment - + collection @refunds, object_root: false + attributes :id, :amount, :created_at, :reason, :comment end diff --git a/bin/bundle b/bin/bundle index 66e9889e..2dbb7176 100755 --- a/bin/bundle +++ b/bin/bundle @@ -1,3 +1,5 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/bin/rails b/bin/rails index 07396602..a31728ab 100755 --- a/bin/rails +++ b/bin/rails @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/bin/rake b/bin/rake index 17240489..c1999550 100755 --- a/bin/rake +++ b/bin/rake @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/bin/setup b/bin/setup index e620b4da..31400462 100755 --- a/bin/setup +++ b/bin/setup @@ -1,10 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require 'pathname' require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") diff --git a/bin/update b/bin/update index a8e4462f..1d6aa6a5 100755 --- a/bin/update +++ b/bin/update @@ -1,10 +1,12 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require 'pathname' require 'fileutils' include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('../../', __FILE__) +APP_ROOT = Pathname.new File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") diff --git a/config.ru b/config.ru index 758e51e1..1155c753 100755 --- a/config.ru +++ b/config.ru @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __FILE__) run Commitchange::Application diff --git a/config/application.rb b/config/application.rb index 91784071..9dd2b4e2 100755 --- a/config/application.rb +++ b/config/application.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require_relative 'boot' @@ -7,84 +9,83 @@ require 'rails/all' # you've limited to :test, :development, or :production. Bundler.require(*Rails.groups) - -#require File.expand_path('lib/htp') # Hamster Table Print +# require File.expand_path('lib/htp') # Hamster Table Print module Commitchange - class Application < Rails::Application - # Settings in config/environments/* take precedence over those specified here. - # Application configuration should go into files in config/initializers - # -- all .rb files in that directory are automatically loaded. + class Application < Rails::Application + # Settings in config/environments/* take precedence over those specified here. + # Application configuration should go into files in config/initializers + # -- all .rb files in that directory are automatically loaded. - # Custom directories with classes and modules you want to be autoloadable. - # config.autoload_paths += %W(#{config.root}/extras) - config.eager_load_paths += Dir["#{config.root}/lib/**/"] + # Custom directories with classes and modules you want to be autoloadable. + # config.autoload_paths += %W(#{config.root}/extras) + config.eager_load_paths += Dir["#{config.root}/lib/**/"] - config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') - config.eager_load_paths += Dir[Rails.root.join('app', 'api', '*')] + config.paths.add File.join('app', 'api'), glob: File.join('**', '*.rb') + config.eager_load_paths += Dir[Rails.root.join('app', 'api', '*')] - # Only load the plugins named here, in the order given (default is alphabetical). - # :all can be used as a placeholder for all plugins not explicitly named. - # config.plugins = [ :exception_notification, :ssl_requirement, :all ] + # Only load the plugins named here, in the order given (default is alphabetical). + # :all can be used as a placeholder for all plugins not explicitly named. + # config.plugins = [ :exception_notification, :ssl_requirement, :all ] - # Activate observers that should always be running. - # config.active_record.observers = :cacher, :garbage_collector, :forum_observer + # Activate observers that should always be running. + # config.active_record.observers = :cacher, :garbage_collector, :forum_observer - # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. - # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. - config.time_zone = 'UTC' + # Set Time.zone default to the specified zone and make Active Record auto-convert to this zone. + # Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC. + config.time_zone = 'UTC' - # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de + # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. + # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] + # config.i18n.default_locale = :de - # Configure the default encoding used in templates for Ruby 1.9. - config.encoding = "utf-8" + # Configure the default encoding used in templates for Ruby 1.9. + config.encoding = 'utf-8' - # Configure sensitive parameters which will be filtered from the log file. - config.filter_parameters += [:password] + # Configure sensitive parameters which will be filtered from the log file. + config.filter_parameters += [:password] - # Enable escaping HTML in JSON. - config.active_support.escape_html_entities_in_json = true + # Enable escaping HTML in JSON. + config.active_support.escape_html_entities_in_json = true - # Use SQL instead of Active Record's schema dumper when creating the database. - # This is necessary if your schema can't be completely dumped by the schema dumper, - # like if you have constraints or database-specific column types - config.active_record.schema_format = :sql + # Use SQL instead of Active Record's schema dumper when creating the database. + # This is necessary if your schema can't be completely dumped by the schema dumper, + # like if you have constraints or database-specific column types + config.active_record.schema_format = :sql - # Enforce whitelist mode for mass assignment. - # This will create an empty whitelist of attributes available for mass-assignment for all models - # in your app. As such, your models will need to explicitly whitelist or blacklist accessible - # parameters by using an attr_accessible or attr_protected declaration. - #config.active_record.whitelist_attributes = true + # Enforce whitelist mode for mass assignment. + # This will create an empty whitelist of attributes available for mass-assignment for all models + # in your app. As such, your models will need to explicitly whitelist or blacklist accessible + # parameters by using an attr_accessible or attr_protected declaration. + # config.active_record.whitelist_attributes = true - # Enable the asset pipeline - config.assets.enabled = true + # Enable the asset pipeline + config.assets.enabled = true - # Precompile all "page" files - config.assets.precompile << Proc.new do |path| - if path =~ /.*page\.(css|js)/ - puts "Compiling asset: " + path - true - else - false - end - end + # Precompile all "page" files + config.assets.precompile << proc do |path| + if /.*page\.(css|js)/.match?(path) + puts 'Compiling asset: ' + path + true + else + false + end + end - # Version of your assets, change this If you want to expire all your assets - # config.assets.version = '1.0' + # Version of your assets, change this If you want to expire all your assets + # config.assets.version = '1.0' - # For Rails 3.1 on Heroku: - # Forces the application to not access the DB - # or load models when precompiling your assets. - # from: devise gem installation instructions/suggestions - # config.assets.initialize_on_precompile = true + # For Rails 3.1 on Heroku: + # Forces the application to not access the DB + # or load models when precompiling your assets. + # from: devise gem installation instructions/suggestions + # config.assets.initialize_on_precompile = true - config.i18n.enforce_available_locales = false + config.i18n.enforce_available_locales = false - # Add trailing slashes to all routes - # config.action_controller.default_url_options = {:trailing_slash => true} - # - # config.browserify_rails.commandline_options = "-t [ babelify --presets es2015 ]" - end + # Add trailing slashes to all routes + # config.action_controller.default_url_options = {:trailing_slash => true} + # + # config.browserify_rails.commandline_options = "-t [ babelify --presets es2015 ]" + end end diff --git a/config/boot.rb b/config/boot.rb index b1a8cf10..62bfc55e 100755 --- a/config/boot.rb +++ b/config/boot.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) require 'bundler/setup' -require 'bootsnap/setup' \ No newline at end of file +require 'bootsnap/setup' diff --git a/config/environment.rb b/config/environment.rb index 85fd23e6..9dd7a2ec 100755 --- a/config/environment.rb +++ b/config/environment.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Load the Rails application require_relative 'application' @@ -6,33 +8,28 @@ Encoding.default_external = Encoding::UTF_8 Encoding.default_internal = Encoding::UTF_8 @ignore_dotenv = ENV['IGNORE_DOTENV'] @env = Rails.env || 'development' -unless (@ignore_dotenv) +unless @ignore_dotenv require 'dotenv' if @env == 'test' - if File.file?(".env.#{@env}") - Dotenv.load ".env.#{@env}" - end + Dotenv.load ".env.#{@env}" if File.file?(".env.#{@env}") else - Dotenv.load ".env" + Dotenv.load '.env' end end @org_name = ENV['ORG_NAME'] || 'default_organization' -puts "config files .env .env.#{@env} ./config/settings.#{@env}.yml#{ @env != 'test' ? " ./config/#{@org_name}.yml": " "} #{ @env != 'test' ? " ./config/#{@org_name}.#{@env}.yml": " "} #{ @env == 'test' ? "./config/settings.test.yml" : ""}" +puts "config files .env .env.#{@env} ./config/settings.#{@env}.yml#{@env != 'test' ? " ./config/#{@org_name}.yml" : ' '} #{@env != 'test' ? " ./config/#{@org_name}.#{@env}.yml" : ' '} #{@env == 'test' ? './config/settings.test.yml' : ''}" if Rails.env == 'test' - Settings.add_source!("./config/settings.test.yml") + Settings.add_source!('./config/settings.test.yml') else Settings.add_source!("./config/#{@org_name}.yml") Settings.add_source!("./config/#{@org_name}.#{Rails.env}.yml") end +# Settings.add_source!("./config/#{@org_name}.#{Rails.env}.yml") - -#Settings.add_source!("./config/#{@org_name}.#{Rails.env}.yml") - -#we load the schema now because we didn't want to do so until we loaded EVERYTHING +# we load the schema now because we didn't want to do so until we loaded EVERYTHING Config.schema do - required(:general).schema do # the name of your website. Default in Settings is "Houdini Project" required(:name).filled(:str?) @@ -45,18 +42,17 @@ Config.schema do # the relative path from asset_host root to your poweredby email logo (PNG, 150px wide) required(:poweredby_logo).filled(:str?) - end required(:default).schema do required(:image).schema do - #the path on your image.host to your default profile image + # the path on your image.host to your default profile image required(:profile).filled(:str?) - #the path on your image.host to your default nonprofit image + # the path on your image.host to your default nonprofit image required(:nonprofit).filled(:str?) - #the path on your image.host to your default campaign background image + # the path on your image.host to your default campaign background image required(:campaign).filled(:str?) end @@ -80,7 +76,7 @@ Config.schema do end required(:mailer).schema do - #an action mailer delivery method + # an action mailer delivery method # Default is sendmail required(:delivery_method).filled(:str?) @@ -132,10 +128,10 @@ Config.schema do optional(:maps).schema do # the map provider to use. Currently that's just Google Maps or nothing # Default is nil - optional(:provider).value(included_in?:['google', nil]) + optional(:provider).value(included_in?: ['google', nil]) optional(:options).schema do - #key for your google maps instance + # key for your google maps instance optional(:key).filled(:str?) end end @@ -144,10 +140,9 @@ Config.schema do # The editor used for editing nonprofit, campaign # and event pages and some email templates # Default is 'quill' - required(:editor).value(included_in?:['quill', 'froala']) + required(:editor).value(included_in?: %w[quill froala]) optional(:editor_options).schema do - # Froala Key if your use froala # Default is nil (you need to get a key) required(:froala_key).filled(:str?) @@ -163,7 +158,7 @@ Config.schema do # Default is 1200 (20 minutes) required(:expiration_time).filled(:int?) - #event donation source tokens are unique. + # event donation source tokens are unique. # The idea is someone may want to donate multiple times at an event without # staff needing to enter their info again. Additionally, they # may want to do it after the event without staff @@ -176,27 +171,25 @@ Config.schema do # The time (in seconds) after an event ends that this token can be used. # Default is 1728000 (20 days) required(:time_after_event).filled(:int?) - end end - #sets the default language for the UI + # sets the default language for the UI required(:language).filled(:str?) - #sets the list of locales available + # sets the list of locales available required(:available_locales).each(:str?) # your default language needs to be in the available locales - rule(make_sure_language_in_locales: [:language, :available_locales]) do |language, available_locales| + rule(make_sure_language_in_locales: %i[language available_locales]) do |language, available_locales| language.included_in?(available_locales) end - # TODO have a way to validate the available_locales are actually available translations + # TODO: have a way to validate the available_locales are actually available translations # Whether to show state fields in the donation wizard optional(:show_state_fields).filled(:bool?) - required(:intntl).schema do # the supporter currencies for the site as abbreviations required(:currencies).each(:str?) @@ -205,17 +198,16 @@ Config.schema do required(:all_currencies).each do # each currency must have the following - # the unit. For 'usd', this would be "dollars" - required(:unit).filled(:str?) - # the abbreviation of the currency. For 'usd', this would be "usd" - required(:abbv).filled(:str?) - # the subunit of the currency. For 'usd', this would be "cents" - required(:subunit).filled(:str?) - # the currency symbol of the currency. For 'usd', this would be "$" - required(:symbol).filled(:str?) - - required(:format).filled(:str?) + # the unit. For 'usd', this would be "dollars" + required(:unit).filled(:str?) + # the abbreviation of the currency. For 'usd', this would be "usd" + required(:abbv).filled(:str?) + # the subunit of the currency. For 'usd', this would be "cents" + required(:subunit).filled(:str?) + # the currency symbol of the currency. For 'usd', this would be "$" + required(:symbol).filled(:str?) + required(:format).filled(:str?) end # an array of country codes to override the default set of countries @@ -228,8 +220,6 @@ Config.schema do # Xavier, I need you document this :) optional(:integration) - - end required(:default_bp).schema do @@ -257,7 +247,7 @@ Config.schema do # complete, corresponding source optional(:ccs).schema do - optional(:ccs_method).value(included_in?: %w(local_tar_gz github)) + optional(:ccs_method).value(included_in?: %w[local_tar_gz github]) # only used for github # NOTE: for github you need to have the hash of the corresponding source in $RAILS_ROOT/CCS_HASH @@ -280,10 +270,9 @@ Config.schema do # the url, absolute or relative, that visitors should be redirected to optional(:maintenance_page).filled(:str?) - end - #the url for your button. As a default, it takes what's in CDN.url + # the url for your button. As a default, it takes what's in CDN.url optional(:button_domain).schema do required(:url).filled?(:str) end @@ -292,7 +281,6 @@ Config.schema do optional(:api_domain).schema do required(:url).filled?(:str) end - end Settings.reload! diff --git a/config/environments/ci.rb b/config/environments/ci.rb index dc55b812..ee0b6c36 100755 --- a/config/environments/ci.rb +++ b/config/environments/ci.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb @@ -20,8 +22,8 @@ Rails.application.configure do # config.action_mailer.default_url_options = { host: 'commitchange.com' } config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } - config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username - config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password + config.action_mailer.smtp_settings['user_name'] = Settings.mailer.username if Settings.mailer.username + config.action_mailer.smtp_settings['password'] = Settings.mailer.password if Settings.mailer.password config.action_mailer.default_url_options = { host: Settings.mailer.host } diff --git a/config/environments/development.rb b/config/environments/development.rb index 8c42eb73..953bd659 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later CarrierWave.configure do |config| config.ignore_integrity_errors = false @@ -38,8 +40,8 @@ Rails.application.configure do # config.action_mailer.default_url_options = { host: 'commitchange.com' } config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } - config.action_mailer.smtp_settings['user_name']= Settings.mailer.username if Settings.mailer.username - config.action_mailer.smtp_settings['password']= Settings.mailer.password if Settings.mailer.password + config.action_mailer.smtp_settings['user_name'] = Settings.mailer.username if Settings.mailer.username + config.action_mailer.smtp_settings['password'] = Settings.mailer.password if Settings.mailer.password config.action_mailer.default_url_options = { host: Settings.mailer.host } @@ -82,5 +84,4 @@ Rails.application.configure do config.dependency_loading = true if $rails_rake_task config.middleware.use I18n::JS::Middleware - end diff --git a/config/environments/production.rb b/config/environments/production.rb index d730f187..821517b3 100755 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -38,7 +40,6 @@ Rails.application.configure do # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' - # Specifies the header that your server uses for sending files. # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for Nginx @@ -50,7 +51,7 @@ Rails.application.configure do config.log_level = :info # Prepend all log lines with the following tags. - config.log_tags = [ :request_id ] + config.log_tags = [:request_id] # Use a different cache store in production. # config.cache_store = :mem_cache_store @@ -79,7 +80,7 @@ Rails.application.configure do # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - if ENV["RAILS_LOG_TO_STDOUT"].present? + if ENV['RAILS_LOG_TO_STDOUT'].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter config.logger = ActiveSupport::TaggedLogging.new(logger) diff --git a/config/environments/staging.rb b/config/environments/staging.rb index d9717aaa..a3904469 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -1,78 +1,79 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Rails.application.configure do - # Settings specified here will take precedence over those in config/application.rb + # Settings specified here will take precedence over those in config/application.rb - # Code is not reloaded between requests - config.cache_classes = true + # Code is not reloaded between requests + config.cache_classes = true - # Full error reports are disabled and caching is turned on - config.consider_all_requests_local = false - config.action_controller.perform_caching = true + # Full error reports are disabled and caching is turned on + config.consider_all_requests_local = false + config.action_controller.perform_caching = true - # Disable Rails's static asset server (Apache or nginx will already do this) - config.serve_static_assets = true + # Disable Rails's static asset server (Apache or nginx will already do this) + config.serve_static_assets = true - # Compress JavaScripts and CSS - config.assets.compress = true + # Compress JavaScripts and CSS + config.assets.compress = true - # Generate digests for assets URLs - config.assets.digest = true + # Generate digests for assets URLs + config.assets.digest = true - # Defaults to nil and saved in location specified by config.assets.prefix - # config.assets.manifest = YOUR_PATH + # Defaults to nil and saved in location specified by config.assets.prefix + # config.assets.manifest = YOUR_PATH - # Specifies the header that your server uses for sending files - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx + # Specifies the header that your server uses for sending files + # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx - # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. - config.force_ssl = true + # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. + config.force_ssl = true # See everything in the log (default is :info) - config.log_level = :info + config.log_level = :info - # Prepend all log lines with the following tags - # config.log_tags = [ :subdomain, :uuid ] + # Prepend all log lines with the following tags + # config.log_tags = [ :subdomain, :uuid ] - # Use a different logger for distributed setups - # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) + # Use a different logger for distributed setups + # config.logger = ActiveSupport::TaggedLogging.new(SyslogLogger.new) - # Use a different cache store in production - # config.cache_store = :mem_cache_store + # Use a different cache store in production + # config.cache_store = :mem_cache_store - # Enable serving of images, stylesheets, and JavaScripts from an asset server - # cdn_url = "https://d2e5we1j08b82a.cloudfront.net" - # config.action_controller.asset_host = cdn_url - # config.action_mailer.asset_host = cdn_url - config.font_assets.origin = '*' + # Enable serving of images, stylesheets, and JavaScripts from an asset server + # cdn_url = "https://d2e5we1j08b82a.cloudfront.net" + # config.action_controller.asset_host = cdn_url + # config.action_mailer.asset_host = cdn_url + config.font_assets.origin = '*' - # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) - # config.assets.precompile = ['application', 'manifests/*'] + # Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added) + # config.assets.precompile = ['application', 'manifests/*'] - # Disable delivery errors, bad email addresses will be ignored - # config.action_mailer.raise_delivery_errors = false - config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym - config.action_mailer.default_url_options = { host: Settings.mailer.host } + # Disable delivery errors, bad email addresses will be ignored + # config.action_mailer.raise_delivery_errors = false + config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym + config.action_mailer.default_url_options = { host: Settings.mailer.host } - # Enable threaded mode - # config.threadsafe! + # Enable threaded mode + # config.threadsafe! - # Enable locale fallbacks for I18n (makes lookups for any locale fall back to - # the I18n.default_locale when a translation can not be found) - config.i18n.fallbacks = true + # Enable locale fallbacks for I18n (makes lookups for any locale fall back to + # the I18n.default_locale when a translation can not be found) + config.i18n.fallbacks = true - # Send deprecation notices to registered listeners - config.active_support.deprecation = :notify + # Send deprecation notices to registered listeners + config.active_support.deprecation = :notify - # Log the query plan for queries taking more than this (works - # with SQLite, MySQL, and PostgreSQL) - # config.active_record.auto_explain_threshold_in_seconds = 0.5 + # Log the query plan for queries taking more than this (works + # with SQLite, MySQL, and PostgreSQL) + # config.active_record.auto_explain_threshold_in_seconds = 0.5 - config.assets.compile = false - - config.threadsafe! - config.dependency_loading = true if $rails_rake_task - # Compress json - # config.middleware.use Rack::Deflater + config.assets.compile = false + config.threadsafe! + config.dependency_loading = true if $rails_rake_task + # Compress json + # config.middleware.use Rack::Deflater end diff --git a/config/environments/test.rb b/config/environments/test.rb index 20bd8332..89d2ed0e 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Commitchange::Application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -39,7 +41,7 @@ Commitchange::Application.configure do # Print deprecation notices to the stderr. config.active_support.deprecation = :stderr - config.action_mailer.default_url_options = {host: 'houdiniproject.test'} + config.action_mailer.default_url_options = { host: 'houdiniproject.test' } # Raises error for missing translations # config.action_view.raise_on_missing_translations = true diff --git a/config/initializers/application_controller_renderer.rb b/config/initializers/application_controller_renderer.rb index 89d2efab..6d56e439 100644 --- a/config/initializers/application_controller_renderer.rb +++ b/config/initializers/application_controller_renderer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # ActiveSupport::Reloader.to_prepare do diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 01ef3e66..678efe9f 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Version of your assets, change this if you want to expire all your assets. diff --git a/config/initializers/aws.rb b/config/initializers/aws.rb index 5335306f..1d534ae9 100644 --- a/config/initializers/aws.rb +++ b/config/initializers/aws.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -AWS.config({ +AWS.config( region: Settings.aws.region, access_key_id: Settings.aws.access_key_id, secret_access_key: Settings.aws.secret_access_key -}) +) s3 = AWS::S3.new S3Bucket = s3.buckets[Settings.aws.bucket] diff --git a/config/initializers/aws_ses.rb b/config/initializers/aws_ses.rb index aa96a921..74e49f1c 100644 --- a/config/initializers/aws_ses.rb +++ b/config/initializers/aws_ses.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later ActionMailer::Base.add_delivery_method :aws_ses, AWS::SES::Base, - access_key_id: ENV['AWS_ACCESS_KEY'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], - server: 'email.us-east-1.amazonaws.com' + access_key_id: ENV['AWS_ACCESS_KEY'], + secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], + server: 'email.us-east-1.amazonaws.com' diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb index e095c2f4..c1827ca8 100755 --- a/config/initializers/backtrace_silencers.rb +++ b/config/initializers/backtrace_silencers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. diff --git a/config/initializers/block_ips.rb b/config/initializers/block_ips.rb index ed9f52f0..8f91a826 100644 --- a/config/initializers/block_ips.rb +++ b/config/initializers/block_ips.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Rack::Attack.blocklist('block charge abusers') do |req| ['54.159.242.229', '54.161.246.233', - '54.211.94.199' - ].include? req.ip + '54.211.94.199'].include? req.ip end diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 18e4d95b..301b8c4b 100755 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later CarrierWave.configure do |config| - config.storage = :aws - config.aws_bucket = Settings.aws.bucket - config.aws_acl = :public_read - config.asset_host = Settings.image&.host || "https://#{Settings.aws.bucket}.s3.amazonaws.com" - config.aws_authenticated_url_expiration = 60 * 60 * 24 * 365 - config.aws_credentials = { - access_key_id: Settings.aws.access_key_id, - secret_access_key: Settings.aws.secret_access_key, - config: AWS.config(cache_dir: "#{Rails.root}/tmp/uploads", region: Settings.aws.region) - } + config.storage = :aws + config.aws_bucket = Settings.aws.bucket + config.aws_acl = :public_read + config.asset_host = Settings.image&.host || "https://#{Settings.aws.bucket}.s3.amazonaws.com" + config.aws_authenticated_url_expiration = 60 * 60 * 24 * 365 + config.aws_credentials = { + access_key_id: Settings.aws.access_key_id, + secret_access_key: Settings.aws.secret_access_key, + config: AWS.config(cache_dir: "#{Rails.root}/tmp/uploads", region: Settings.aws.region) + } end diff --git a/config/initializers/chunked_uploader.rb b/config/initializers/chunked_uploader.rb index e0066c98..30d041c3 100644 --- a/config/initializers/chunked_uploader.rb +++ b/config/initializers/chunked_uploader.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -CHUNKED_UPLOADER = ENV['CHUNKED_UPLOAD_CLASS'] ? ENV['CHUNKED_UPLOAD_CLASS'].constantize : ChunkedUploader::S3 \ No newline at end of file +CHUNKED_UPLOADER = ENV['CHUNKED_UPLOAD_CLASS'] ? ENV['CHUNKED_UPLOAD_CLASS'].constantize : ChunkedUploader::S3 diff --git a/config/initializers/config.rb b/config/initializers/config.rb index b253585e..665b270a 100644 --- a/config/initializers/config.rb +++ b/config/initializers/config.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Config.setup do |config| # Name of the constant exposing loaded settings diff --git a/config/initializers/cookies_serializer.rb b/config/initializers/cookies_serializer.rb index 5a6a32d3..ee8dff9c 100644 --- a/config/initializers/cookies_serializer.rb +++ b/config/initializers/cookies_serializer.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Specify a serializer for the signed and encrypted cookie jars. diff --git a/config/initializers/delayed_job_config.rb b/config/initializers/delayed_job_config.rb index 08ff944b..4f673b2d 100644 --- a/config/initializers/delayed_job_config.rb +++ b/config/initializers/delayed_job_config.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Delayed::Worker.max_attempts = 1 diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index c140c3dd..97faf6e5 100755 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -1,229 +1,229 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. Devise.setup do |config| - # ==> Mailer Configuration - # Configure the e-mail address which will be shown in Devise::Mailer, - # note that it will be overwritten if you use your own mailer class with default "from" parameter. - config.mailer_sender = Settings.devise.mailer_sender + # ==> Mailer Configuration + # Configure the e-mail address which will be shown in Devise::Mailer, + # note that it will be overwritten if you use your own mailer class with default "from" parameter. + config.mailer_sender = Settings.devise.mailer_sender - # Configure the class responsible to send e-mails. - # config.mailer = "Devise::Mailer" + # Configure the class responsible to send e-mails. + # config.mailer = "Devise::Mailer" - # ==> ORM configuration - # Load and configure the ORM. Supports :active_record (default) and - # :mongoid (bson_ext recommended) by default. Other ORMs may be - # available as additional gems. - require 'devise/orm/active_record' + # ==> ORM configuration + # Load and configure the ORM. Supports :active_record (default) and + # :mongoid (bson_ext recommended) by default. Other ORMs may be + # available as additional gems. + require 'devise/orm/active_record' - # ==> Configuration for any authentication mechanism - # Configure which keys are used when authenticating a user. The default is - # just :email. You can configure it to use [:username, :subdomain], so for - # authenticating a user, both parameters are required. Remember that those - # parameters are used only when authenticating and not when retrieving from - # session. If you need permissions, you should implement that in a before filter. - # You can also supply a hash where the value is a boolean determining whether - # or not authentication should be aborted when the value is not present. - # config.authentication_keys = [ :email ] + # ==> Configuration for any authentication mechanism + # Configure which keys are used when authenticating a user. The default is + # just :email. You can configure it to use [:username, :subdomain], so for + # authenticating a user, both parameters are required. Remember that those + # parameters are used only when authenticating and not when retrieving from + # session. If you need permissions, you should implement that in a before filter. + # You can also supply a hash where the value is a boolean determining whether + # or not authentication should be aborted when the value is not present. + # config.authentication_keys = [ :email ] - # Configure parameters from the request object used for authentication. Each entry - # given should be a request method and it will automatically be passed to the - # find_for_authentication method and considered in your model lookup. For instance, - # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. - # The same considerations mentioned for authentication_keys also apply to request_keys. - # config.request_keys = [] + # Configure parameters from the request object used for authentication. Each entry + # given should be a request method and it will automatically be passed to the + # find_for_authentication method and considered in your model lookup. For instance, + # if you set :request_keys to [:subdomain], :subdomain will be used on authentication. + # The same considerations mentioned for authentication_keys also apply to request_keys. + # config.request_keys = [] - # Configure which authentication keys should be case-insensitive. - # These keys will be downcased upon creating or modifying a user and when used - # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [ :email ] + # Configure which authentication keys should be case-insensitive. + # These keys will be downcased upon creating or modifying a user and when used + # to authenticate or find a user. Default is :email. + config.case_insensitive_keys = [:email] - # Configure which authentication keys should have whitespace stripped. - # These keys will have whitespace before and after removed upon creating or - # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [ :email ] + # Configure which authentication keys should have whitespace stripped. + # These keys will have whitespace before and after removed upon creating or + # modifying a user and when used to authenticate or find a user. Default is :email. + config.strip_whitespace_keys = [:email] - # Tell if authentication through request.params is enabled. True by default. - # It can be set to an array that will enable params authentication only for the - # given strategies, for example, `config.params_authenticatable = [:database]` will - # enable it only for database (email + password) authentication. - # config.params_authenticatable = true + # Tell if authentication through request.params is enabled. True by default. + # It can be set to an array that will enable params authentication only for the + # given strategies, for example, `config.params_authenticatable = [:database]` will + # enable it only for database (email + password) authentication. + # config.params_authenticatable = true - # Tell if authentication through HTTP Basic Auth is enabled. False by default. - # It can be set to an array that will enable http authentication only for the - # given strategies, for example, `config.http_authenticatable = [:token]` will - # enable it only for token authentication. - # config.http_authenticatable = false + # Tell if authentication through HTTP Basic Auth is enabled. False by default. + # It can be set to an array that will enable http authentication only for the + # given strategies, for example, `config.http_authenticatable = [:token]` will + # enable it only for token authentication. + # config.http_authenticatable = false - # If http headers should be returned for AJAX requests. True by default. - # config.http_authenticatable_on_xhr = true + # If http headers should be returned for AJAX requests. True by default. + # config.http_authenticatable_on_xhr = true - # The realm used in Http Basic Authentication. "Application" by default. - # config.http_authentication_realm = "Application" + # The realm used in Http Basic Authentication. "Application" by default. + # config.http_authentication_realm = "Application" - # It will change confirmation, password recovery and other workflows - # to behave the same regardless if the e-mail provided was right or wrong. - # Does not affect registerable. - # config.paranoid = true + # It will change confirmation, password recovery and other workflows + # to behave the same regardless if the e-mail provided was right or wrong. + # Does not affect registerable. + # config.paranoid = true - # By default Devise will store the user in session. You can skip storage for - # :http_auth and :token_auth by adding those symbols to the array below. - # Notice that if you are skipping storage for all authentication paths, you - # may want to disable generating routes to Devise's sessions controller by - # passing :skip => :sessions to `devise_for` in your config/routes.rb - config.skip_session_storage = [:http_auth] + # By default Devise will store the user in session. You can skip storage for + # :http_auth and :token_auth by adding those symbols to the array below. + # Notice that if you are skipping storage for all authentication paths, you + # may want to disable generating routes to Devise's sessions controller by + # passing :skip => :sessions to `devise_for` in your config/routes.rb + config.skip_session_storage = [:http_auth] - # ==> Configuration for :database_authenticatable - # For bcrypt, this is the cost for hashing the password and defaults to 10. If - # using other encryptors, it sets how many times you want the password re-encrypted. - # - # Limiting the stretches to just one in testing will increase the performance of - # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use - # a value less than 10 in other environments. - config.stretches = Rails.env.test? ? 1 : 10 + # ==> Configuration for :database_authenticatable + # For bcrypt, this is the cost for hashing the password and defaults to 10. If + # using other encryptors, it sets how many times you want the password re-encrypted. + # + # Limiting the stretches to just one in testing will increase the performance of + # your test suite dramatically. However, it is STRONGLY RECOMMENDED to not use + # a value less than 10 in other environments. + config.stretches = Rails.env.test? ? 1 : 10 + # ==> Configuration for :confirmable + # A period that the user is allowed to access the website even without + # confirming his account. For instance, if set to 2.days, the user will be + # able to access the website for two days without confirming his account, + # access will be blocked just in the third day. Default is 0.days, meaning + # the user cannot access the website without confirming his account. + # config.allow_unconfirmed_access_for = 2.days - # ==> Configuration for :confirmable - # A period that the user is allowed to access the website even without - # confirming his account. For instance, if set to 2.days, the user will be - # able to access the website for two days without confirming his account, - # access will be blocked just in the third day. Default is 0.days, meaning - # the user cannot access the website without confirming his account. - # config.allow_unconfirmed_access_for = 2.days + # If true, requires any email changes to be confirmed (exactly the same way as + # initial account confirmation) to be applied. Requires additional unconfirmed_email + # db field (see migrations). Until confirmed new email is stored in + # unconfirmed email column, and copied to email column on successful confirmation. + config.reconfirmable = true - # If true, requires any email changes to be confirmed (exactly the same way as - # initial account confirmation) to be applied. Requires additional unconfirmed_email - # db field (see migrations). Until confirmed new email is stored in - # unconfirmed email column, and copied to email column on successful confirmation. - config.reconfirmable = true + # Defines which key will be used when confirming an account + # config.confirmation_keys = [ :email ] - # Defines which key will be used when confirming an account - # config.confirmation_keys = [ :email ] + # ==> Configuration for :rememberable + # The time the user will be remembered without asking for credentials again. + # config.remember_for = 2.weeks - # ==> Configuration for :rememberable - # The time the user will be remembered without asking for credentials again. - # config.remember_for = 2.weeks + # If true, extends the user's remember period when remembered via cookie. + # config.extend_remember_period = false - # If true, extends the user's remember period when remembered via cookie. - # config.extend_remember_period = false + # Options to be passed to the created cookie. For instance, you can set + # :secure => true in order to force SSL only cookies. + # config.rememberable_options = {} - # Options to be passed to the created cookie. For instance, you can set - # :secure => true in order to force SSL only cookies. - # config.rememberable_options = {} + # ==> Configuration for :validatable + # Range for password length. Default is 6..128. + # config.password_length = 6..128 - # ==> Configuration for :validatable - # Range for password length. Default is 6..128. - # config.password_length = 6..128 + # Email regex used to validate email formats. It simply asserts that + # an one (and only one) @ exists in the given string. This is mainly + # to give user feedback and not to assert the e-mail validity. + # config.email_regexp = /\A[^@]+@[^@]+\z/ - # Email regex used to validate email formats. It simply asserts that - # an one (and only one) @ exists in the given string. This is mainly - # to give user feedback and not to assert the e-mail validity. - # config.email_regexp = /\A[^@]+@[^@]+\z/ + # ==> Configuration for :timeoutable + # The time you want to timeout the user session without activity. After this + # time the user will be asked for credentials again. Default is 30 minutes. + # config.timeout_in = 30.minutes - # ==> Configuration for :timeoutable - # The time you want to timeout the user session without activity. After this - # time the user will be asked for credentials again. Default is 30 minutes. - # config.timeout_in = 30.minutes + # If true, expires auth token on session timeout. + # config.expire_auth_token_on_timeout = false - # If true, expires auth token on session timeout. - # config.expire_auth_token_on_timeout = false + # ==> Configuration for :lockable + # Defines which strategy will be used to lock an account. + # :failed_attempts = Locks an account after a number of failed attempts to sign in. + # :none = No lock strategy. You should handle locking by yourself. + # config.lock_strategy = :failed_attempts - # ==> Configuration for :lockable - # Defines which strategy will be used to lock an account. - # :failed_attempts = Locks an account after a number of failed attempts to sign in. - # :none = No lock strategy. You should handle locking by yourself. - # config.lock_strategy = :failed_attempts + # Defines which key will be used when locking and unlocking an account + # config.unlock_keys = [ :email ] - # Defines which key will be used when locking and unlocking an account - # config.unlock_keys = [ :email ] + # Defines which strategy will be used to unlock an account. + # :email = Sends an unlock link to the user email + # :time = Re-enables login after a certain amount of time (see :unlock_in below) + # :both = Enables both strategies + # :none = No unlock strategy. You should handle unlocking by yourself. + # config.unlock_strategy = :both - # Defines which strategy will be used to unlock an account. - # :email = Sends an unlock link to the user email - # :time = Re-enables login after a certain amount of time (see :unlock_in below) - # :both = Enables both strategies - # :none = No unlock strategy. You should handle unlocking by yourself. - # config.unlock_strategy = :both + # Number of authentication tries before locking an account if lock_strategy + # is failed attempts. + # config.maximum_attempts = 20 - # Number of authentication tries before locking an account if lock_strategy - # is failed attempts. - # config.maximum_attempts = 20 + # Time interval to unlock the account if :time is enabled as unlock_strategy. + # config.unlock_in = 1.hour - # Time interval to unlock the account if :time is enabled as unlock_strategy. - # config.unlock_in = 1.hour + # ==> Configuration for :recoverable + # + # Defines which key will be used when recovering the password for an account + # config.reset_password_keys = [ :email ] - # ==> Configuration for :recoverable - # - # Defines which key will be used when recovering the password for an account - # config.reset_password_keys = [ :email ] + # Time interval you can reset your password with a reset password key. + # Don't put a too small interval or your users won't have the time to + # change their passwords. + config.reset_password_within = 6.hours - # Time interval you can reset your password with a reset password key. - # Don't put a too small interval or your users won't have the time to - # change their passwords. - config.reset_password_within = 6.hours + # ==> Configuration for :encryptable + # Allow you to use another encryption algorithm besides bcrypt (default). You can use + # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, + # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) + # and :restful_authentication_sha1 (then you should set stretches to 10, and copy + # REST_AUTH_SITE_KEY to pepper) + # config.encryptor = :sha512 - # ==> Configuration for :encryptable - # Allow you to use another encryption algorithm besides bcrypt (default). You can use - # :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1, - # :authlogic_sha512 (then you should set stretches above to 20 for default behavior) - # and :restful_authentication_sha1 (then you should set stretches to 10, and copy - # REST_AUTH_SITE_KEY to pepper) - # config.encryptor = :sha512 + # ==> Configuration for :token_authenticatable + # Defines name of the authentication token params key + # config.token_authentication_key = :auth_token - # ==> Configuration for :token_authenticatable - # Defines name of the authentication token params key - # config.token_authentication_key = :auth_token + # ==> Scopes configuration + # Turn scoped views on. Before rendering "sessions/new", it will first check for + # "users/sessions/new". It's turned off by default because it's slower if you + # are using only default views. + # config.scoped_views = false - # ==> Scopes configuration - # Turn scoped views on. Before rendering "sessions/new", it will first check for - # "users/sessions/new". It's turned off by default because it's slower if you - # are using only default views. - # config.scoped_views = false + # Configure the default scope given to Warden. By default it's the first + # devise role declared in your routes (usually :user). + # config.default_scope = :user - # Configure the default scope given to Warden. By default it's the first - # devise role declared in your routes (usually :user). - # config.default_scope = :user + # Set this configuration to false if you want /users/sign_out to sign out + # only the current scope. By default, Devise signs out all scopes. + # config.sign_out_all_scopes = true - # Set this configuration to false if you want /users/sign_out to sign out - # only the current scope. By default, Devise signs out all scopes. - # config.sign_out_all_scopes = true + # ==> Navigation configuration + # Lists the formats that should be treated as navigational. Formats like + # :html, should redirect to the sign in page when the user does not have + # access, but formats like :xml or :json, should return 401. + # + # If you have any extra navigational formats, like :iphone or :mobile, you + # should add them to the navigational formats lists. + # + # The "*/*" below is required to match Internet Explorer requests. + # config.navigational_formats = ["*/*", :html] - # ==> Navigation configuration - # Lists the formats that should be treated as navigational. Formats like - # :html, should redirect to the sign in page when the user does not have - # access, but formats like :xml or :json, should return 401. - # - # If you have any extra navigational formats, like :iphone or :mobile, you - # should add them to the navigational formats lists. - # - # The "*/*" below is required to match Internet Explorer requests. - # config.navigational_formats = ["*/*", :html] + # The default HTTP method used to sign out a resource. Default is :delete. + config.sign_out_via = :get - # The default HTTP method used to sign out a resource. Default is :delete. - config.sign_out_via = :get + config.secret_key = ENV.fetch('DEVISE_SECRET_KEY') + # ==> Warden configuration + # If you want to use other strategies, that are not supported by Devise, or + # change the failure app, you can configure them inside the config.warden block. + # + # config.warden do |manager| + # manager.intercept_401 = false + # manager.default_strategies(:scope => :user).unshift :some_external_strategy + # end - config.secret_key = ENV.fetch('DEVISE_SECRET_KEY') - - # ==> Warden configuration - # If you want to use other strategies, that are not supported by Devise, or - # change the failure app, you can configure them inside the config.warden block. - # - # config.warden do |manager| - # manager.intercept_401 = false - # manager.default_strategies(:scope => :user).unshift :some_external_strategy - # end - - # ==> Mountable engine configurations - # When using Devise inside an engine, let's call it `MyEngine`, and this engine - # is mountable, there are some extra configurations to be taken into account. - # The following options are available, assuming the engine is mounted as: - # - # mount MyEngine, at: "/my_engine" - # - # The router that invoked `devise_for`, in the example above, would be: - # config.router_name = :my_engine - # - # When using omniauth, Devise cannot automatically set Omniauth path, - # so you need to do it manually. For the users scope, it would be: - # config.omniauth_path_prefix = "/my_engine/users/auth" + # ==> Mountable engine configurations + # When using Devise inside an engine, let's call it `MyEngine`, and this engine + # is mountable, there are some extra configurations to be taken into account. + # The following options are available, assuming the engine is mounted as: + # + # mount MyEngine, at: "/my_engine" + # + # The router that invoked `devise_for`, in the example above, would be: + # config.router_name = :my_engine + # + # When using omniauth, Devise cannot automatically set Omniauth path, + # so you need to do it manually. For the users scope, it would be: + # config.omniauth_path_prefix = "/my_engine/users/auth" end diff --git a/config/initializers/devise_async.rb b/config/initializers/devise_async.rb index 42d81a4e..08a6cce0 100644 --- a/config/initializers/devise_async.rb +++ b/config/initializers/devise_async.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -#Devise::Async.backend = :delayed_job +# Devise::Async.backend = :delayed_job diff --git a/config/initializers/email_jobs.rb b/config/initializers/email_jobs.rb index a327a3a2..6523df69 100644 --- a/config/initializers/email_jobs.rb +++ b/config/initializers/email_jobs.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -MAX_EMAIL_JOB_ATTEMPTS = Rails.env == 'production' ? 50 : 2 \ No newline at end of file +MAX_EMAIL_JOB_ATTEMPTS = Rails.env == 'production' ? 50 : 2 diff --git a/config/initializers/filter_parameter_logging.rb b/config/initializers/filter_parameter_logging.rb index 4a994e1e..7a4f47b4 100644 --- a/config/initializers/filter_parameter_logging.rb +++ b/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/config/initializers/fullcontact.rb b/config/initializers/fullcontact.rb index aba9e144..780bfaaa 100644 --- a/config/initializers/fullcontact.rb +++ b/config/initializers/fullcontact.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require "fullcontact" +require 'fullcontact' FullContact.configure do |config| - config.api_key = ENV['FULL_CONTACT_KEY'] + config.api_key = ENV['FULL_CONTACT_KEY'] end -# see gem docs: https://github.com/fullcontact/fullcontact-api-ruby \ No newline at end of file +# see gem docs: https://github.com/fullcontact/fullcontact-api-ruby diff --git a/config/initializers/geocode.rb b/config/initializers/geocode.rb index aa034b73..0d7a0ed5 100644 --- a/config/initializers/geocode.rb +++ b/config/initializers/geocode.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Geocoder.configure({ +Geocoder.configure( cache: Rails.cache, lookup: :google, use_https: true, api_key: ENV['GOOGLE_API_KEY'], timeout: 10 -}) +) diff --git a/config/initializers/hamster_extensions.rb b/config/initializers/hamster_extensions.rb index 0181ba95..bd096a29 100644 --- a/config/initializers/hamster_extensions.rb +++ b/config/initializers/hamster_extensions.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Hamster extesions/modifications # Default Hamster to_json methods don't work right module ProperJson - def to_json(options={}) + def to_json(options = {}) Hamster.to_ruby(self).to_json(options) end end diff --git a/config/initializers/inflections.rb b/config/initializers/inflections.rb index ac033bf9..dc847422 100755 --- a/config/initializers/inflections.rb +++ b/config/initializers/inflections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index 44eafa22..e86b5d39 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later I18n.available_locales = Settings.available_locales Rails.application.config.i18n.fallbacks = [I18n.default_locale] diff --git a/config/initializers/log_rage.rb b/config/initializers/log_rage.rb index f76666a7..99017ba9 100644 --- a/config/initializers/log_rage.rb +++ b/config/initializers/log_rage.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Rails.application.configure do - if (Rails.env != 'test') + if Rails.env != 'test' - - config.lograge.enabled = true - # add time to lograge - config.lograge.custom_options = lambda do |event| - { time: event.time, - exception: event.payload[:exception], # ["ExceptionClass", "the message"] - exception_object: event.payload[:exception_object] # the exception instance - } + config.lograge.enabled = true + # add time to lograge + config.lograge.custom_options = lambda do |event| + { time: event.time, + exception: event.payload[:exception], # ["ExceptionClass", "the message"] + exception_object: event.payload[:exception_object] } # the exception instance + end end - end -end \ No newline at end of file +end diff --git a/config/initializers/mailchimp.rb b/config/initializers/mailchimp.rb index b727aef8..34ec6aaa 100644 --- a/config/initializers/mailchimp.rb +++ b/config/initializers/mailchimp.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'mailchimp' -Mailchimp.config({ - :api_key => ENV['MAILCHIMP_API_KEY'] -}) +Mailchimp.config( + api_key: ENV['MAILCHIMP_API_KEY'] +) diff --git a/config/initializers/mime_types.rb b/config/initializers/mime_types.rb index dc189968..be6fedc5 100755 --- a/config/initializers/mime_types.rb +++ b/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/config/initializers/pg_type_map.rb b/config/initializers/pg_type_map.rb index 1072e823..ecb9dd8b 100644 --- a/config/initializers/pg_type_map.rb +++ b/config/initializers/pg_type_map.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'active_record' require 'qx' @@ -8,6 +10,5 @@ rescue ActiveRecord::NoDatabaseError false else Qx.config(type_map: PG::BasicTypeMapForResults.new(ActiveRecord::Base.connection.raw_connection)) - Qx.execute("SET TIME ZONE utc") + Qx.execute('SET TIME ZONE utc') end - diff --git a/config/initializers/quiet_assets.rb b/config/initializers/quiet_assets.rb index 7bb65cb7..b0be9f6f 100644 --- a/config/initializers/quiet_assets.rb +++ b/config/initializers/quiet_assets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later if Rails.env.development? # Rails.application.assets.logger = Logger.new('/dev/null') diff --git a/config/initializers/rabl_init.rb b/config/initializers/rabl_init.rb index acd7ebaa..8d1da391 100644 --- a/config/initializers/rabl_init.rb +++ b/config/initializers/rabl_init.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rabl' Rabl.configure do |config| - config.enable_json_callbacks = true + config.enable_json_callbacks = true end diff --git a/config/initializers/reload_api.rb b/config/initializers/reload_api.rb index 41e476bc..8ec0657e 100644 --- a/config/initializers/reload_api.rb +++ b/config/initializers/reload_api.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + if Rails.env.development? ActiveSupport::Dependencies.explicitly_unloadable_constants << 'Houdini::V1' diff --git a/config/initializers/secret_token.rb b/config/initializers/secret_token.rb index 10dc8dd1..0d405334 100755 --- a/config/initializers/secret_token.rb +++ b/config/initializers/secret_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Be sure to restart your server when you modify this file. @@ -5,4 +7,4 @@ # If you change this key, all old signed cookies will become invalid! # Make sure the secret is at least 30 characters and all random, # no regular words or you'll be exposed to dictionary attacks. -Rails.application.config.secret_token = ENV.fetch('SECRET_TOKEN') +Rails.application.config.secret_key_base = ENV.fetch('SECRET_TOKEN') diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb index 0fa3b506..c46dbdec 100755 --- a/config/initializers/session_store.rb +++ b/config/initializers/session_store.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. Rails.application.config.session_store :cookie_store, key: '_commitchange_session' diff --git a/config/initializers/slack_notice.rb b/config/initializers/slack_notice.rb index 3f6519e7..2d8f127d 100644 --- a/config/initializers/slack_notice.rb +++ b/config/initializers/slack_notice.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # This will fire a slack event when charges are greater than $3 Million # We can then remove this file and/or update any notice we'd like @@ -10,4 +12,4 @@ # if charges >= 300000000 # notifier = Slack::Notifier.new webhook_url, channel: '#general', username: 'CommitChange' # notifier.ping " We've processed more than $3,000,000 dollars!" -# end \ No newline at end of file +# end diff --git a/config/initializers/stripe.rb b/config/initializers/stripe.rb index 8148222c..e51bc8f7 100644 --- a/config/initializers/stripe.rb +++ b/config/initializers/stripe.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'stripe' -Stripe.api_key = Settings.payment_provider.stripe_private_key; +Stripe.api_key = Settings.payment_provider.stripe_private_key diff --git a/config/initializers/time.rb b/config/initializers/time.rb index ebef3a83..d8dd8755 100644 --- a/config/initializers/time.rb +++ b/config/initializers/time.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later ENV['TZ'] = 'UTC' Time.zone = 'UTC' diff --git a/config/initializers/timeout.rb b/config/initializers/timeout.rb index e1cc5c7c..69b4f74b 100644 --- a/config/initializers/timeout.rb +++ b/config/initializers/timeout.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # timeout = Integer(ENV['WEB_TIMEOUT'] || 15) # if ENV['RAILS_ENV'] == 'development' || ENV['IDE_PROCESS_DISPATCHER'] # timeout = 10000 # end # -# Rack::Timeout.timeout = timeout # seconds \ No newline at end of file +# Rack::Timeout.timeout = timeout # seconds diff --git a/config/initializers/wrap_parameters.rb b/config/initializers/wrap_parameters.rb index c72a9676..8447298a 100755 --- a/config/initializers/wrap_parameters.rb +++ b/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which diff --git a/config/puma.rb b/config/puma.rb index b93dad33..f3eaa09e 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Puma can serve each request in a thread from an internal thread pool. @@ -6,19 +8,17 @@ # the maximum value specified for Puma. Default is set to 5 threads for minimum # and maximum, this matches the default thread size of Active Record. # -threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }.to_i +threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }.to_i threads threads_count, threads_count preload_app! if ENV['RAILS_ENV'] != 'development' rackup DefaultRackup -port ENV.fetch("PORT") { 5000 } -environment ENV.fetch('RAILS_ENV'){ 'development' } +port ENV.fetch('PORT') { 5000 } +environment ENV.fetch('RAILS_ENV') { 'development' } workers Integer(ENV['WEB_CONCURRENCY'] || 1) - - on_worker_boot do # ActiveSupport.on_load(:active_record) do # config = ActiveRecord::Base.configurations[Rails.env] || @@ -31,4 +31,3 @@ end # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart - diff --git a/config/routes.rb b/config/routes.rb index 32c60eb3..287f964d 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,266 +1,260 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Rails.application.routes.draw do mount Houdini::API => '/api' if Rails.env == 'development' - get '/button_debug/embedded' => 'button_debug#embedded' - get '/button_debug/button' => 'button_debug#button' - get '/button_debug/embedded/:id' => 'button_debug#embedded' - get '/button_debug/button/:id' => 'button_debug#button' + get '/button_debug/embedded' => 'button_debug#embedded' + get '/button_debug/button' => 'button_debug#button' + get '/button_debug/embedded/:id' => 'button_debug#embedded' + get '/button_debug/button/:id' => 'button_debug#button' end get 'onboard' => 'onboard#index' - resources(:emails, {only: [:create]}) - resources(:settings, {only: [:index]}) - resources(:campaign_gifts, {only: [:create]}) - resource(:cards, {only: [:create, :update, :destroy]}) - resource(:direct_debit_details, {path: 'sepa', controller: :direct_debit_details, only: [:create]}) - # resources(:activities, {only: [:create]}) + resources(:emails, only: [:create]) + resources(:settings, only: [:index]) + resources(:campaign_gifts, only: [:create]) + resource(:cards, only: %i[create update destroy]) + resource(:direct_debit_details, path: 'sepa', controller: :direct_debit_details, only: [:create]) + # resources(:activities, {only: [:create]}) # Creating presigned posts for direct-to-S3 upload - resources(:aws_presigned_posts, {only: [:create]}) + resources(:aws_presigned_posts, only: [:create]) - resources(:image_attachments, {only: [:create]}) do - post(:remove, {on: :collection}) - end - - resources(:profiles, {only: [:show, :update]}) do - get(:fundraisers, {on: :member}) - get(:events, {on: :member}) - get(:donations_history, {on: :member}) - end - - - namespace(:nonprofits, {path: 'nonprofits/:nonprofit_id'}) do - resources(:payouts, {only: [:create, :index, :show]}) - resources(:imports, {only: [:create]}) - resources(:nonprofit_keys, {only: [:index]}) do - get(:mailchimp_login, {on: :collection}) - get(:mailchimp_landing, {on: :collection}) - end - resources(:reports, {only: []}) do - get(:end_of_year, {on: :collection}) - get(:end_of_year_custom, {on: :collection}) - end - resources(:email_lists, {only: [:index, :create]}) - resources(:payments, {only: [:index, :show, :update, :destroy]}) do - post(:export, {on: :collection}) - post(:resend_donor_receipt, {on: :member}) - post(:resend_admin_receipt, {on: :member}) - - end - resources(:donations, {only: [:index, :show, :create, :update]}) do - put(:followup, {on: :member}) - post(:create_offsite, {on: :collection}) - end - resource(:card, {only: [:edit, :update, :show, :create]}) - - resources(:charges, {only: [:index]}) do - resources(:refunds, {only: [:create, :index]}) - end - - resource(:bank_account, {only: [:create]}) do - get(:confirmation) - post(:confirm) - get(:cancellation) - post(:cancel) - post(:resend_confirmation) - end - - resources(:supporter_emails, {only: [:create, :show]}) do - post(:gmail, {on: :collection}) - end - - resources(:custom_field_masters, {only: [:index, :create, :destroy]}) - resources(:custom_field_joins, {only: []}) do - post(:modify, {on: :collection}) - end - - resources(:tag_masters, {only: [:index, :create, :destroy]}) - resources(:tag_joins, {only: []}) do - post(:modify, {on: :collection}) - end - - resources(:supporters, {only: [:index, :show, :create, :update, :new]}) do - resources(:tag_joins, {only: [:index, :destroy]}) - resources(:custom_field_joins, {only: [:index, :destroy]}) - resources(:supporter_notes, {only: [:create, :update, :destroy]}) - resources(:activities, {only: [:index]}) - post(:export, {on: :collection}) - put :bulk_delete, on: :collection - post :merge, on: :collection - get :merge_data, on: :collection - get :info_card, on: :member - get :email_address, on: :member - get :full_contact, on: :member - get :index_metrics, on: :collection - end - - resources(:recurring_donations, {only: [:index, :show, :destroy, :update, :create]}) do - post(:export, on: :collection) - end - - resource(:miscellaneous_np_info, {only: [:show, :update]}) - - namespace(:button) do - root({action: :advanced}) - get(:basic) - get(:guided) - get(:advanced) - post(:send_code) - end - - post 'tracking', controller: 'trackings', action: 'create' - end - - namespace(:campaigns, {path: '/nonprofits/:nonprofit_id/campaigns/:campaign_id/admin', only: []}) do - resources(:supporters, {only: [:index]}) - resources(:donations, {only: [:index]}) - resources(:campaign_gift_options, {only: [:index]}) + resources(:image_attachments, only: [:create]) do + post(:remove, on: :collection) end - resources(:nonprofits, {only: [:show, :create, :update, :destroy]}) do - post(:onboard, {on: :collection}) - get(:profile_todos, {on: :member}) - get(:recurring_donation_stats, {on: :member}) - get(:search, {on: :collection}) - get(:dashboard_todos, {on: :member}) - put(:verify_identity, {on: :member}) + resources(:profiles, only: %i[show update]) do + get(:fundraisers, on: :member) + get(:events, on: :member) + get(:donations_history, on: :member) + end + namespace(:nonprofits, path: 'nonprofits/:nonprofit_id') do + resources(:payouts, only: %i[create index show]) + resources(:imports, only: [:create]) + resources(:nonprofit_keys, only: [:index]) do + get(:mailchimp_login, on: :collection) + get(:mailchimp_landing, on: :collection) + end + resources(:reports, only: []) do + get(:end_of_year, on: :collection) + get(:end_of_year_custom, on: :collection) + end + resources(:email_lists, only: %i[index create]) + resources(:payments, only: %i[index show update destroy]) do + post(:export, on: :collection) + post(:resend_donor_receipt, on: :member) + post(:resend_admin_receipt, on: :member) + end + resources(:donations, only: %i[index show create update]) do + put(:followup, on: :member) + post(:create_offsite, on: :collection) + end + resource(:card, only: %i[edit update show create]) - resources(:roles, {only: [:create, :destroy]}) - resources(:settings, {only: [:index]}) - resources(:pricing, {only: [:index]}) - resources(:email_settings, {only: [:index, :create]}) - resources(:users, {only: [:index, :create]}) do - resources(:email_settings, {only: [:index, :create]}) + resources(:charges, only: [:index]) do + resources(:refunds, only: %i[create index]) end - resources(:campaigns, {only: [:index, :show, :create, :update]}) do - get(:metrics, {on: :member}) - get(:totals, {on: :member}) - get(:timeline, {on: :member}) - post(:duplicate, {on: :member}) - get(:activities, {on: :member}) - put(:soft_delete, {on: :member}) - get(:name_and_id, {on: :collection}) - post :create_from_template, on: :collection - resources(:campaign_gift_options, {only: [:index, :show, :create, :update, :destroy]}) do - put(:update_order, {on: :collection}) - end - end - - resource(:billing_subscription, {only: [:create]}) do - post(:cancel) - post(:create_trial, {on: :member}) + resource(:bank_account, only: [:create]) do + get(:confirmation) + post(:confirm) get(:cancellation) - end + post(:cancel) + post(:resend_confirmation) + end - resources(:events, {only: [:index, :show, :create, :update]}) do - get(:metrics, {on: :member}) - get(:listings, {on: :collection}) - get(:stats, {on: :member}) - get(:name_and_id, {on: :collection}) - get(:activities, {on: :member}) - post(:duplicate, {on: :member}) - put(:soft_delete) - resources(:tickets, {only: [:create, :update, :index, :destroy]}) do - put(:add_note, {on: :member}) - post(:delete_card_for_ticket, {on: :member}) - end - resources(:ticket_levels, {only: [:index, :show, :create, :update, :destroy]}) do - put(:update_order, {on: :collection}) - end - resources(:event_discounts, {only: [:create, :index, :update, :destroy]}) - end + resources(:supporter_emails, only: %i[create show]) do + post(:gmail, on: :collection) + end - get(:donate, {on: :member}) - get(:btn, {on: :member}) - get(:supporter_form, {on: :member}) - post(:custom_supporter, {on: :member}) - get(:dashboard, {on: :member}) - get(:dashboard_metrics, {on: :member}) - get(:payment_history, {on: :member}) + resources(:custom_field_masters, only: %i[index create destroy]) + resources(:custom_field_joins, only: []) do + post(:modify, on: :collection) + end - post(:donate, {on: :member, as: 'create_donation'}) - end + resources(:tag_masters, only: %i[index create destroy]) + resources(:tag_joins, only: []) do + post(:modify, on: :collection) + end - resources(:recurring_donations, {only: [:edit, :destroy, :update]}) do - put(:update_amount, {on: :member}) + resources(:supporters, only: %i[index show create update new]) do + resources(:tag_joins, only: %i[index destroy]) + resources(:custom_field_joins, only: %i[index destroy]) + resources(:supporter_notes, only: %i[create update destroy]) + resources(:activities, only: [:index]) + post(:export, on: :collection) + put :bulk_delete, on: :collection + post :merge, on: :collection + get :merge_data, on: :collection + get :info_card, on: :member + get :email_address, on: :member + get :full_contact, on: :member + get :index_metrics, on: :collection + end + + resources(:recurring_donations, only: %i[index show destroy update create]) do + post(:export, on: :collection) + end + + resource(:miscellaneous_np_info, only: %i[show update]) + + namespace(:button) do + root(action: :advanced) + get(:basic) + get(:guided) + get(:advanced) + post(:send_code) + end + + post 'tracking', controller: 'trackings', action: 'create' end - devise_for :users, - :controllers => { - :sessions => 'users/sessions', - :registrations => 'users/registrations', - :confirmations => 'users/confirmations' - } - devise_scope :user do - match '/sign_in' => 'users/sessions#new', via: [:get, :post] - match '/signup' => 'devise/registrations#new', via: [:get, :post] - post '/confirm' => 'users/confirmations#confirm', via: [:get] - match '/users/is_confirmed' => 'users/confirmations#is_confirmed', via: [:get, :post] - match '/users/exists' => 'users/confirmations#exists', via: [:get] - post '/users/confirm_auth', action: :confirm_auth, controller: 'users/sessions', via: [:get, :post] - end + namespace(:campaigns, path: '/nonprofits/:nonprofit_id/campaigns/:campaign_id/admin', only: []) do + resources(:supporters, only: [:index]) + resources(:donations, only: [:index]) + resources(:campaign_gift_options, only: [:index]) + end - # Super admin - match '/admin' => 'super_admins#index', :as => 'admin', via: [:get, :post] - match '/admin/search-nonprofits' => 'super_admins#search_nonprofits', via: [:get, :post] - match '/admin/search-profiles' => 'super_admins#search_profiles', via: [:get, :post] - match '/admin/search-fullcontact' => 'super_admins#search_fullcontact', via: [:get, :post] - match '/admin/recurring-donations-without-cards' => 'super_admins#recurring_donations_without_cards', via: [:get, :post] - match '/admin/export_supporters_with_rds' => 'super_admins#export_supporters_with_rds', via: [:get, :post] - match '/admin/resend_user_confirmation' => 'super_admins#resend_user_confirmation', via: [:get, :post] + resources(:nonprofits, only: %i[show create update destroy]) do + post(:onboard, on: :collection) + get(:profile_todos, on: :member) + get(:recurring_donation_stats, on: :member) + get(:search, on: :collection) + get(:dashboard_todos, on: :member) + put(:verify_identity, on: :member) + + resources(:roles, only: %i[create destroy]) + resources(:settings, only: [:index]) + resources(:pricing, only: [:index]) + resources(:email_settings, only: %i[index create]) + resources(:users, only: %i[index create]) do + resources(:email_settings, only: %i[index create]) + end + + resources(:campaigns, only: %i[index show create update]) do + get(:metrics, on: :member) + get(:totals, on: :member) + get(:timeline, on: :member) + post(:duplicate, on: :member) + get(:activities, on: :member) + put(:soft_delete, on: :member) + get(:name_and_id, on: :collection) + post :create_from_template, on: :collection + resources(:campaign_gift_options, only: %i[index show create update destroy]) do + put(:update_order, on: :collection) + end + end + + resource(:billing_subscription, only: [:create]) do + post(:cancel) + post(:create_trial, on: :member) + get(:cancellation) + end + + resources(:events, only: %i[index show create update]) do + get(:metrics, on: :member) + get(:listings, on: :collection) + get(:stats, on: :member) + get(:name_and_id, on: :collection) + get(:activities, on: :member) + post(:duplicate, on: :member) + put(:soft_delete) + resources(:tickets, only: %i[create update index destroy]) do + put(:add_note, on: :member) + post(:delete_card_for_ticket, on: :member) + end + resources(:ticket_levels, only: %i[index show create update destroy]) do + put(:update_order, on: :collection) + end + resources(:event_discounts, only: %i[create index update destroy]) + end + + get(:donate, on: :member) + get(:btn, on: :member) + get(:supporter_form, on: :member) + post(:custom_supporter, on: :member) + get(:dashboard, on: :member) + get(:dashboard_metrics, on: :member) + get(:payment_history, on: :member) + + post(:donate, on: :member, as: 'create_donation') + end + + resources(:recurring_donations, only: %i[edit destroy update]) do + put(:update_amount, on: :member) + end + + devise_for :users, + controllers: { + sessions: 'users/sessions', + registrations: 'users/registrations', + confirmations: 'users/confirmations' + } + devise_scope :user do + match '/sign_in' => 'users/sessions#new', via: %i[get post] + match '/signup' => 'devise/registrations#new', via: %i[get post] + post '/confirm' => 'users/confirmations#confirm', via: [:get] + match '/users/is_confirmed' => 'users/confirmations#is_confirmed', via: %i[get post] + match '/users/exists' => 'users/confirmations#exists', via: [:get] + post '/users/confirm_auth', action: :confirm_auth, controller: 'users/sessions', via: %i[get post] + end + + # Super admin + match '/admin' => 'super_admins#index', :as => 'admin', via: %i[get post] + match '/admin/search-nonprofits' => 'super_admins#search_nonprofits', via: %i[get post] + match '/admin/search-profiles' => 'super_admins#search_profiles', via: %i[get post] + match '/admin/search-fullcontact' => 'super_admins#search_fullcontact', via: %i[get post] + match '/admin/recurring-donations-without-cards' => 'super_admins#recurring_donations_without_cards', via: %i[get post] + match '/admin/export_supporters_with_rds' => 'super_admins#export_supporters_with_rds', via: %i[get post] + match '/admin/resend_user_confirmation' => 'super_admins#resend_user_confirmation', via: %i[get post] # Events match '/events' => 'events#index', via: [:get] - match '/events/:event_slug' => 'events#show', via: [:get, :post] + match '/events/:event_slug' => 'events#show', via: %i[get post] - # Nonprofits - match ':state_code/:city/:name' => 'nonprofits#show', :as => :nonprofit_location, via: [:get, :post] - match ':state_code/:city/:name/donate' => 'nonprofits#donate', :as => :nonprofit_donation, via: [:get, :post] - match ':state_code/:city/:name/button' => 'nonprofits/button#guided', via: [:get, :post] + # Nonprofits + match ':state_code/:city/:name' => 'nonprofits#show', :as => :nonprofit_location, via: %i[get post] + match ':state_code/:city/:name/donate' => 'nonprofits#donate', :as => :nonprofit_donation, via: %i[get post] + match ':state_code/:city/:name/button' => 'nonprofits/button#guided', via: %i[get post] - # Campaigns - match ':state_code/:city/:name/campaigns' => 'campaigns#index', via: [:get, :post] - match ':state_code/:city/:name/campaigns/:campaign_slug' => 'campaigns#show', via: [:get, :post] - match ':state_code/:city/:name/campaigns/:campaign_slug/supporters' => 'campaigns/supporters#index', via: [:get, :post] - match '/peer-to-peer' => 'campaigns#peer_to_peer', via: [:get, :post] + # Campaigns + match ':state_code/:city/:name/campaigns' => 'campaigns#index', via: %i[get post] + match ':state_code/:city/:name/campaigns/:campaign_slug' => 'campaigns#show', via: %i[get post] + match ':state_code/:city/:name/campaigns/:campaign_slug/supporters' => 'campaigns/supporters#index', via: %i[get post] + match '/peer-to-peer' => 'campaigns#peer_to_peer', via: %i[get post] - # Events - match ':state_code/:city/:name/events' => 'events#index', via: [:get, :post] - match ':state_code/:city/:name/events/:event_slug' => 'events#show', via: [:get, :post] - match ':state_code/:city/:name/events/:event_slug/stats' => 'events#stats', via: [:get, :post] - match ':state_code/:city/:name/events/:event_slug/tickets' => 'tickets#index', via: [:get, :post] - # get '/events' => 'events#index' + # Events + match ':state_code/:city/:name/events' => 'events#index', via: %i[get post] + match ':state_code/:city/:name/events/:event_slug' => 'events#show', via: %i[get post] + match ':state_code/:city/:name/events/:event_slug/stats' => 'events#stats', via: %i[get post] + match ':state_code/:city/:name/events/:event_slug/tickets' => 'tickets#index', via: %i[get post] + # get '/events' => 'events#index' - # Dashboard - match ':state_code/:city/:name/dashboard' => 'nonprofits#dashboard', as: :np_dashboard, via: [:get, :post] + # Dashboard + match ':state_code/:city/:name/dashboard' => 'nonprofits#dashboard', as: :np_dashboard, via: %i[get post] - # Misc - get '/pages/wp-plugin', to: redirect('/help/wordpress-plugin') #temporary, until WP plugin updated + # Misc + get '/pages/wp-plugin', to: redirect('/help/wordpress-plugin') # temporary, until WP plugin updated - # Maps - get '/maps/all-npos' => 'maps#all_npos' - get '/maps/all-supporters' => 'maps#all_supporters' - get '/maps/all-npo-supporters' => 'maps#all_npo_supporters' - get '/maps/specific-npo-supporters' => 'maps#specific_npo_supporters' + # Maps + get '/maps/all-npos' => 'maps#all_npos' + get '/maps/all-supporters' => 'maps#all_supporters' + get '/maps/all-npo-supporters' => 'maps#all_npo_supporters' + get '/maps/specific-npo-supporters' => 'maps#specific_npo_supporters' - # Mailchimp Landing - match '/mailchimp-landing' => 'nonprofits/nonprofit_keys#mailchimp_landing', via: [:get, :post] + # Mailchimp Landing + match '/mailchimp-landing' => 'nonprofits/nonprofit_keys#mailchimp_landing', via: %i[get post] # Webhooks post '/webhooks/stripe_subscription_payment' => 'webhooks#subscription_payment' post '/webhooks/stripe' => 'webhooks#stripe' get '/static/terms_and_privacy' => 'static#terms_and_privacy' - get '/static/ccs' => 'static#ccs' - - - - root :to => 'front#index' - - + get '/static/ccs' => 'static#ccs' + root to: 'front#index' end diff --git a/config/spring.rb b/config/spring.rb index c9119b40..c5933e49 100644 --- a/config/spring.rb +++ b/config/spring.rb @@ -1,6 +1,8 @@ -%w( +# frozen_string_literal: true + +%w[ .ruby-version .rbenv-vars tmp/restart.txt tmp/caching-dev.txt -).each { |path| Spring.watch(path) } +].each { |path| Spring.watch(path) } diff --git a/db/migrate/20170307222633_add_indexes_for_payment_and_supporter_queries.rb b/db/migrate/20170307222633_add_indexes_for_payment_and_supporter_queries.rb index 36b5ac5d..c9fc64eb 100644 --- a/db/migrate/20170307222633_add_indexes_for_payment_and_supporter_queries.rb +++ b/db/migrate/20170307222633_add_indexes_for_payment_and_supporter_queries.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexesForPaymentAndSupporterQueries < ActiveRecord::Migration def up Qx.transaction do - Qx.execute(%Q( + Qx.execute(%( CREATE INDEX IF NOT EXISTS payments_date ON payments (date); CREATE INDEX IF NOT EXISTS payments_gross_amount ON payments (gross_amount); CREATE INDEX IF NOT EXISTS payments_kind ON payments (kind); @@ -29,28 +31,28 @@ class AddIndexesForPaymentAndSupporterQueries < ActiveRecord::Migration end def down - Qx.execute(%Q( - DROP INDEX IF EXISTS payments_date; - DROP INDEX IF EXISTS payments_gross_amount; - DROP INDEX IF EXISTS payments_kind; - DROP INDEX IF EXISTS payments_towards; - DROP INDEX IF EXISTS payments_supporter_id; - DROP INDEX IF EXISTS payments_nonprofit_id; + Qx.execute(%( + DROP INDEX IF EXISTS payments_date; + DROP INDEX IF EXISTS payments_gross_amount; + DROP INDEX IF EXISTS payments_kind; + DROP INDEX IF EXISTS payments_towards; + DROP INDEX IF EXISTS payments_supporter_id; + DROP INDEX IF EXISTS payments_nonprofit_id; - DROP INDEX IF EXISTS supporters_created_at; - DROP INDEX IF EXISTS supporters_name; - DROP INDEX IF EXISTS supporters_email; - DROP INDEX IF EXISTS supporters_nonprofit_id; - DROP INDEX IF EXISTS supporters_donation_id; + DROP INDEX IF EXISTS supporters_created_at; + DROP INDEX IF EXISTS supporters_name; + DROP INDEX IF EXISTS supporters_email; + DROP INDEX IF EXISTS supporters_nonprofit_id; + DROP INDEX IF EXISTS supporters_donation_id; - DROP INDEX IF EXISTS donations_amount; - DROP INDEX IF EXISTS donations_designation; - DROP INDEX IF EXISTS donations_supporter_id; + DROP INDEX IF EXISTS donations_amount; + DROP INDEX IF EXISTS donations_designation; + DROP INDEX IF EXISTS donations_supporter_id; - DROP INDEX IF EXISTS tag_joins_supporter_id; - DROP INDEX IF EXISTS tag_joins_tag_master_id; + DROP INDEX IF EXISTS tag_joins_supporter_id; + DROP INDEX IF EXISTS tag_joins_tag_master_id; - DROP INDEX IF EXISTS custom_field_joins_custom_field_master_id; - )) + DROP INDEX IF EXISTS custom_field_joins_custom_field_master_id; + )) end end diff --git a/db/migrate/20170307223525_drop_all_cruft.rb b/db/migrate/20170307223525_drop_all_cruft.rb index 92361fc7..316d8194 100644 --- a/db/migrate/20170307223525_drop_all_cruft.rb +++ b/db/migrate/20170307223525_drop_all_cruft.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class DropAllCruft < ActiveRecord::Migration def change - Qx.execute(%Q( + Qx.execute(%( DROP FUNCTION IF EXISTS update_payment_donations_search_vectors(); DROP FUNCTION IF EXISTS supporters_insert_trigger(); DROP FUNCTION IF EXISTS update_payment_search_vectors(); @@ -22,7 +24,7 @@ class DropAllCruft < ActiveRecord::Migration end def down - Qx.execute(%Q( + Qx.execute(%( CREATE FUNCTION update_payment_donations_search_vectors() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN @@ -42,9 +44,9 @@ SELECT payments.id, concat_ws(' ' , donations.designation , donations.dedication ) AS search_blob -FROM payments +FROM payments LEFT OUTER JOIN supporters - ON payments.supporter_id=supporters.id + ON payments.supporter_id=supporters.id LEFT OUTER JOIN donations ON payments.donation_id=donations.id WHERE (payments.donation_id=NEW.id)) AS data @@ -53,7 +55,7 @@ WHERE (payments.donation_id=NEW.id)) AS data END $$; )) - Qx.execute(%Q( + Qx.execute(%( CREATE FUNCTION supporters_insert_trigger() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -63,7 +65,7 @@ CREATE FUNCTION supporters_insert_trigger() RETURNS trigger END; $$; )) - Qx.execute(%Q( + Qx.execute(%( CREATE FUNCTION update_payment_search_vectors() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN @@ -83,9 +85,9 @@ SELECT payments.id, concat_ws(' ' , donations.designation , donations.dedication ) AS search_blob -FROM payments +FROM payments LEFT OUTER JOIN supporters - ON payments.supporter_id=supporters.id + ON payments.supporter_id=supporters.id LEFT OUTER JOIN donations ON payments.donation_id=donations.id WHERE (payments.id=NEW.id)) AS data @@ -95,7 +97,7 @@ WHERE (payments.id=NEW.id)) AS data )) - Qx.execute(%Q( + Qx.execute(%( CREATE FUNCTION update_payment_supporters_search_vectors() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN @@ -115,9 +117,9 @@ SELECT payments.id, concat_ws(' ' , donations.designation , donations.dedication ) AS search_blob -FROM payments +FROM payments LEFT OUTER JOIN supporters - ON payments.supporter_id=supporters.id + ON payments.supporter_id=supporters.id LEFT OUTER JOIN donations ON payments.donation_id=donations.id WHERE (payments.supporter_id=NEW.id)) AS data @@ -126,8 +128,7 @@ WHERE (payments.supporter_id=NEW.id)) AS data END $$; )) - - Qx.execute(%Q( + Qx.execute(%( CREATE FUNCTION update_supporter_search_vectors() RETURNS trigger LANGUAGE plpgsql AS $$ BEGIN @@ -148,14 +149,14 @@ SELECT supporters.id, concat_ws(' ' , payments.kind , payments.towards ) AS search_blob -FROM supporters +FROM supporters LEFT OUTER JOIN payments - ON payments.supporter_id=supporters.id + ON payments.supporter_id=supporters.id LEFT OUTER JOIN donations - ON donations.supporter_id=supporters.id + ON donations.supporter_id=supporters.id LEFT OUTER JOIN ( SELECT string_agg(value::text, ' ') AS value, supporter_id -FROM custom_field_joins +FROM custom_field_joins GROUP BY supporter_id) AS custom_field_joins ON custom_field_joins.supporter_id=supporters.id WHERE (supporters.id=NEW.id)) AS data @@ -164,8 +165,7 @@ WHERE (supporters.id=NEW.id)) AS data END $$; )) - - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE billing_customers ( id integer NOT NULL, card_name character varying(255), @@ -177,7 +177,7 @@ CREATE TABLE billing_customers ( )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE coupons ( id integer NOT NULL, name character varying(255), @@ -189,7 +189,7 @@ CREATE TABLE coupons ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE dedications ( id integer NOT NULL, donation_id integer, @@ -199,7 +199,7 @@ CREATE TABLE dedications ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE email_drafts ( id integer NOT NULL, nonprofit_id integer, @@ -211,7 +211,7 @@ CREATE TABLE email_drafts ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE image_points ( id integer NOT NULL, image_name character varying(255), @@ -226,7 +226,7 @@ CREATE TABLE image_points ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE pg_search_documents ( id integer NOT NULL, content text, @@ -238,7 +238,7 @@ CREATE TABLE pg_search_documents ( )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE prospect_events ( id integer NOT NULL, event character varying(255), @@ -249,7 +249,7 @@ CREATE TABLE prospect_events ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE prospect_visit_params ( id integer NOT NULL, key character varying(255), @@ -258,7 +258,7 @@ CREATE TABLE prospect_visit_params ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE prospect_visits ( id integer NOT NULL, pathname text, @@ -268,7 +268,7 @@ CREATE TABLE prospect_visits ( ); )) - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE prospects ( id integer NOT NULL, ip_address character varying(255), @@ -282,8 +282,7 @@ CREATE TABLE prospect_visits ( ); )) - - Qx.execute(%Q( + Qx.execute(%( CREATE TABLE recommendations ( id integer NOT NULL, nonprofit_id integer, @@ -293,9 +292,5 @@ CREATE TABLE recommendations ( updated_at timestamp without time zone NOT NULL ); )) - - - end - end diff --git a/db/migrate/20170314193744_normalize_start_and_end_datetimes_for_events_and_campaigns.rb b/db/migrate/20170314193744_normalize_start_and_end_datetimes_for_events_and_campaigns.rb index 15227178..9b234ea2 100644 --- a/db/migrate/20170314193744_normalize_start_and_end_datetimes_for_events_and_campaigns.rb +++ b/db/migrate/20170314193744_normalize_start_and_end_datetimes_for_events_and_campaigns.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NormalizeStartAndEndDatetimesForEventsAndCampaigns < ActiveRecord::Migration def up @@ -5,11 +7,11 @@ class NormalizeStartAndEndDatetimesForEventsAndCampaigns < ActiveRecord::Migrati add_column :events, :end_datetime, :datetime add_column :campaigns, :end_datetime, :datetime Qx.update(:events) - .set(%Q(start_datetime = ("date" + start_time), end_datetime = ("date" + end_time))) + .set(%(start_datetime = ("date" + start_time), end_datetime = ("date" + end_time))) .where("created_at > '2012-01-01'") .execute Qx.update(:campaigns) - .set("end_datetime = (expiration + end_time)") + .set('end_datetime = (expiration + end_time)') .where("created_at > '2012-01-01'") .execute remove_column :events, :end_time @@ -18,6 +20,7 @@ class NormalizeStartAndEndDatetimesForEventsAndCampaigns < ActiveRecord::Migrati remove_column :campaigns, :expiration remove_column :campaigns, :end_time end + def down add_column :events, :end_time, :time add_column :events, :start_time, :time @@ -25,11 +28,11 @@ class NormalizeStartAndEndDatetimesForEventsAndCampaigns < ActiveRecord::Migrati add_column :campaigns, :expiration, :date add_column :campaigns, :end_time, :time Qx.update(:events) - .set(%Q(end_time = end_datetime::time, start_time = start_datetime::time, "date" = start_datetime::date)) + .set(%(end_time = end_datetime::time, start_time = start_datetime::time, "date" = start_datetime::date)) .where("created_at > '2012-01-01'") .execute Qx.update(:campaigns) - .set("end_time = end_datetime::time, expiration = end_datetime::date") + .set('end_time = end_datetime::time, expiration = end_datetime::date') .where("created_at > '2012-01-01'") .execute remove_column :events, :start_datetime diff --git a/db/migrate/20170322203228_add_donation_campaign_id_index.rb b/db/migrate/20170322203228_add_donation_campaign_id_index.rb index 8fa5a6d8..d797be9a 100644 --- a/db/migrate/20170322203228_add_donation_campaign_id_index.rb +++ b/db/migrate/20170322203228_add_donation_campaign_id_index.rb @@ -1,15 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddDonationCampaignIdIndex < ActiveRecord::Migration def up - Qx.execute(%Q( + Qx.execute(%( CREATE INDEX IF NOT EXISTS donations_campaign_id ON donations (campaign_id); CREATE INDEX IF NOT EXISTS donations_event_id ON donations (event_id); )) end + def down - Qx.execute(%Q( - DROP INDEX IF EXISTS donations_campaign_id; - DROP INDEX IF EXISTS donations_event_id; - )) + Qx.execute(%( + DROP INDEX IF EXISTS donations_campaign_id; + DROP INDEX IF EXISTS donations_event_id; + )) end end diff --git a/db/migrate/20170805180556_add_the_tag_joins_backup_table.rb b/db/migrate/20170805180556_add_the_tag_joins_backup_table.rb index a77dfa34..c694e906 100644 --- a/db/migrate/20170805180556_add_the_tag_joins_backup_table.rb +++ b/db/migrate/20170805180556_add_the_tag_joins_backup_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddTheTagJoinsBackupTable < ActiveRecord::Migration def up diff --git a/db/migrate/20170805180557_add_index_for_tag_joins_and_add_constraint.rb b/db/migrate/20170805180557_add_index_for_tag_joins_and_add_constraint.rb index fb22e96c..87b86365 100644 --- a/db/migrate/20170805180557_add_index_for_tag_joins_and_add_constraint.rb +++ b/db/migrate/20170805180557_add_index_for_tag_joins_and_add_constraint.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexForTagJoinsAndAddConstraint < ActiveRecord::Migration def up ids = DeleteTagJoins.find_multiple_tag_joins DeleteTagJoins.copy_and_delete(ids) - add_index :tag_joins, [:tag_master_id, :supporter_id], :unique => true, :name => 'tag_join_supporter_unique_idx' + add_index :tag_joins, %i[tag_master_id supporter_id], unique: true, name: 'tag_join_supporter_unique_idx' end def down - remove_index(:tag_joins, :name => 'tag_join_supporter_unique_idx') + remove_index(:tag_joins, name: 'tag_join_supporter_unique_idx') DeleteTagJoins.revert end end diff --git a/db/migrate/20170805180558_add_custom_field_joins_backup_table.rb b/db/migrate/20170805180558_add_custom_field_joins_backup_table.rb index db8187a9..eb4f70a6 100644 --- a/db/migrate/20170805180558_add_custom_field_joins_backup_table.rb +++ b/db/migrate/20170805180558_add_custom_field_joins_backup_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddCustomFieldJoinsBackupTable < ActiveRecord::Migration def change diff --git a/db/migrate/20170805180559_add_index_for_custom_field_join_and_supporters.rb b/db/migrate/20170805180559_add_index_for_custom_field_join_and_supporters.rb index 7da226d6..052fe66f 100644 --- a/db/migrate/20170805180559_add_index_for_custom_field_join_and_supporters.rb +++ b/db/migrate/20170805180559_add_index_for_custom_field_join_and_supporters.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexForCustomFieldJoinAndSupporters < ActiveRecord::Migration def up ids = DeleteCustomFieldJoins.find_multiple_custom_field_joins DeleteCustomFieldJoins.copy_and_delete(ids) - add_index :custom_field_joins, [:custom_field_master_id, :supporter_id], :unique => true, :name => 'custom_field_join_supporter_unique_idx' + add_index :custom_field_joins, %i[custom_field_master_id supporter_id], unique: true, name: 'custom_field_join_supporter_unique_idx' end def down - remove_index(:custom_field_joins, :name => 'custom_field_join_supporter_unique_idx') + remove_index(:custom_field_joins, name: 'custom_field_join_supporter_unique_idx') DeleteCustomFieldJoins.revert end end diff --git a/db/migrate/20170808180559_add_inactive_to_card.rb b/db/migrate/20170808180559_add_inactive_to_card.rb index 6b18d4cf..7721eaae 100644 --- a/db/migrate/20170808180559_add_inactive_to_card.rb +++ b/db/migrate/20170808180559_add_inactive_to_card.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddInactiveToCard < ActiveRecord::Migration class Card < ActiveRecord::Base @@ -6,8 +8,8 @@ class AddInactiveToCard < ActiveRecord::Migration def change add_column :cards, :inactive, :boolean - add_index :cards, [:id, :holder_type, :holder_id, :inactive] # add index for getting active_card + add_index :cards, %i[id holder_type holder_id inactive] # add index for getting active_card Card.reset_column_information - Card.update_all(:inactive => false) + Card.update_all(inactive: false) end end diff --git a/db/migrate/20170818201127_create_exports.rb b/db/migrate/20170818201127_create_exports.rb index c1b0e4d1..1524e10f 100644 --- a/db/migrate/20170818201127_create_exports.rb +++ b/db/migrate/20170818201127_create_exports.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CreateExports < ActiveRecord::Migration def change diff --git a/db/migrate/20171002160808_create_miscellaneous_np_infos.rb b/db/migrate/20171002160808_create_miscellaneous_np_infos.rb index f157816a..2aba8d26 100644 --- a/db/migrate/20171002160808_create_miscellaneous_np_infos.rb +++ b/db/migrate/20171002160808_create_miscellaneous_np_infos.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CreateMiscellaneousNpInfos < ActiveRecord::Migration def change diff --git a/db/migrate/20171002164402_add_currency_to_nonprofit.rb b/db/migrate/20171002164402_add_currency_to_nonprofit.rb index d03545a5..f275aaf6 100644 --- a/db/migrate/20171002164402_add_currency_to_nonprofit.rb +++ b/db/migrate/20171002164402_add_currency_to_nonprofit.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddCurrencyToNonprofit < ActiveRecord::Migration def change diff --git a/db/migrate/20171004203633_add_change_amount_message_to_miscellaneous_np_infos.rb b/db/migrate/20171004203633_add_change_amount_message_to_miscellaneous_np_infos.rb index 67a7ffa4..5f276a2a 100644 --- a/db/migrate/20171004203633_add_change_amount_message_to_miscellaneous_np_infos.rb +++ b/db/migrate/20171004203633_add_change_amount_message_to_miscellaneous_np_infos.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddChangeAmountMessageToMiscellaneousNpInfos < ActiveRecord::Migration def change diff --git a/db/migrate/20171010204437_add_first_and_last_name_to_supporter.rb b/db/migrate/20171010204437_add_first_and_last_name_to_supporter.rb index 7ec499cb..66c239f8 100644 --- a/db/migrate/20171010204437_add_first_and_last_name_to_supporter.rb +++ b/db/migrate/20171010204437_add_first_and_last_name_to_supporter.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddFirstAndLastNameToSupporter < ActiveRecord::Migration def change diff --git a/db/migrate/20171016181942_add_tracking.rb b/db/migrate/20171016181942_add_tracking.rb index bab82503..fa1b68c3 100644 --- a/db/migrate/20171016181942_add_tracking.rb +++ b/db/migrate/20171016181942_add_tracking.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddTracking < ActiveRecord::Migration def change diff --git a/db/migrate/20171024133806_add_queued_for_import_at_to_donation.rb b/db/migrate/20171024133806_add_queued_for_import_at_to_donation.rb index e0162a77..fc969eab 100644 --- a/db/migrate/20171024133806_add_queued_for_import_at_to_donation.rb +++ b/db/migrate/20171024133806_add_queued_for_import_at_to_donation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddQueuedForImportAtToDonation < ActiveRecord::Migration def change diff --git a/db/migrate/20171026102139_add_direct_debit_detail.rb b/db/migrate/20171026102139_add_direct_debit_detail.rb index 634dd7ee..d9bcdde7 100644 --- a/db/migrate/20171026102139_add_direct_debit_detail.rb +++ b/db/migrate/20171026102139_add_direct_debit_detail.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddDirectDebitDetail < ActiveRecord::Migration def change @@ -11,9 +13,9 @@ class AddDirectDebitDetail < ActiveRecord::Migration end add_column :donations, - :direct_debit_detail_id, - :integer, - index: true, - references: :direct_debit_details + :direct_debit_detail_id, + :integer, + index: true, + references: :direct_debit_details end end diff --git a/db/migrate/20171129215957_add_utm_content_to_trackings.rb b/db/migrate/20171129215957_add_utm_content_to_trackings.rb index 3831186c..5047fb6c 100644 --- a/db/migrate/20171129215957_add_utm_content_to_trackings.rb +++ b/db/migrate/20171129215957_add_utm_content_to_trackings.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddUtmContentToTrackings < ActiveRecord::Migration def change diff --git a/db/migrate/20171130182254_add_locale_to_supporters.rb b/db/migrate/20171130182254_add_locale_to_supporters.rb index 1d0c8c6e..8f31d9dc 100644 --- a/db/migrate/20171130182254_add_locale_to_supporters.rb +++ b/db/migrate/20171130182254_add_locale_to_supporters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddLocaleToSupporters < ActiveRecord::Migration def change diff --git a/db/migrate/20171130193955_add_payment_provider_to_donations.rb b/db/migrate/20171130193955_add_payment_provider_to_donations.rb index f6d335f5..a4c399dc 100644 --- a/db/migrate/20171130193955_add_payment_provider_to_donations.rb +++ b/db/migrate/20171130193955_add_payment_provider_to_donations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddPaymentProviderToDonations < ActiveRecord::Migration def change diff --git a/db/migrate/20171206113317_add_direct_debit_detail_to_charges.rb b/db/migrate/20171206113317_add_direct_debit_detail_to_charges.rb index 6734e2dd..2b4dce85 100644 --- a/db/migrate/20171206113317_add_direct_debit_detail_to_charges.rb +++ b/db/migrate/20171206113317_add_direct_debit_detail_to_charges.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddDirectDebitDetailToCharges < ActiveRecord::Migration def change diff --git a/db/migrate/20171207114229_add_external_identifier_to_campaign.rb b/db/migrate/20171207114229_add_external_identifier_to_campaign.rb index 9aedb86a..75f1f95b 100644 --- a/db/migrate/20171207114229_add_external_identifier_to_campaign.rb +++ b/db/migrate/20171207114229_add_external_identifier_to_campaign.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddExternalIdentifierToCampaign < ActiveRecord::Migration def change diff --git a/db/migrate/20171207191435_add_index_to_campaign_gifts.rb b/db/migrate/20171207191435_add_index_to_campaign_gifts.rb index e834f105..21034bdf 100644 --- a/db/migrate/20171207191435_add_index_to_campaign_gifts.rb +++ b/db/migrate/20171207191435_add_index_to_campaign_gifts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexToCampaignGifts < ActiveRecord::Migration def change diff --git a/db/migrate/20171207191712_add_index_to_activities.rb b/db/migrate/20171207191712_add_index_to_activities.rb index 14ef08a2..d851dbb8 100644 --- a/db/migrate/20171207191712_add_index_to_activities.rb +++ b/db/migrate/20171207191712_add_index_to_activities.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexToActivities < ActiveRecord::Migration def change diff --git a/db/migrate/20171207200746_modify_supporters_name_index.rb b/db/migrate/20171207200746_modify_supporters_name_index.rb index 16c5d2eb..a4c920d7 100644 --- a/db/migrate/20171207200746_modify_supporters_name_index.rb +++ b/db/migrate/20171207200746_modify_supporters_name_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ModifySupportersNameIndex < ActiveRecord::Migration def up diff --git a/db/migrate/20171207200950_add_supporters_name_index.rb b/db/migrate/20171207200950_add_supporters_name_index.rb index 150eca64..0ac0d94f 100644 --- a/db/migrate/20171207200950_add_supporters_name_index.rb +++ b/db/migrate/20171207200950_add_supporters_name_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddSupportersNameIndex < ActiveRecord::Migration def change diff --git a/db/migrate/20171207210431_add_charges_payment_id_index.rb b/db/migrate/20171207210431_add_charges_payment_id_index.rb index 8d41dfcf..4d612e02 100644 --- a/db/migrate/20171207210431_add_charges_payment_id_index.rb +++ b/db/migrate/20171207210431_add_charges_payment_id_index.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddChargesPaymentIdIndex < ActiveRecord::Migration def change diff --git a/db/migrate/20180106024119_add_indexes_for_supporter_deleted_and_import.rb b/db/migrate/20180106024119_add_indexes_for_supporter_deleted_and_import.rb index 98e4263c..b72d9a6a 100644 --- a/db/migrate/20180106024119_add_indexes_for_supporter_deleted_and_import.rb +++ b/db/migrate/20180106024119_add_indexes_for_supporter_deleted_and_import.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddIndexesForSupporterDeletedAndImport < ActiveRecord::Migration def change diff --git a/db/migrate/20180119215653_create_payment_imports.rb b/db/migrate/20180119215653_create_payment_imports.rb index c80e8bf5..113ca313 100644 --- a/db/migrate/20180119215653_create_payment_imports.rb +++ b/db/migrate/20180119215653_create_payment_imports.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CreatePaymentImports < ActiveRecord::Migration def change diff --git a/db/migrate/20180119215913_create_donations_payment_imports_join_table.rb b/db/migrate/20180119215913_create_donations_payment_imports_join_table.rb index 82b8db95..7cba7c3e 100644 --- a/db/migrate/20180119215913_create_donations_payment_imports_join_table.rb +++ b/db/migrate/20180119215913_create_donations_payment_imports_join_table.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CreateDonationsPaymentImportsJoinTable < ActiveRecord::Migration def change diff --git a/db/migrate/20180202181929_remove_unused_metadata.rb b/db/migrate/20180202181929_remove_unused_metadata.rb index 182f0642..dd8e4e2f 100644 --- a/db/migrate/20180202181929_remove_unused_metadata.rb +++ b/db/migrate/20180202181929_remove_unused_metadata.rb @@ -1,48 +1,48 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RemoveUnusedMetadata < ActiveRecord::Migration - - TABLES = [ - :campaign_gift_options, - :campaign_gifts, - :campaigns, - :cards, - :charges, - :custom_field_masters, - :custom_field_joins, - :disputes, - :donations, - :events, - :imports, - :nonprofits, - :offsite_payments, - :payments, - :payment_payouts, - :payouts, - :profiles, - :recurring_donations, - :refunds, - :roles, - :supporter_emails, - :supporter_notes, - :supporters, - :tag_joins, - :tag_masters, - :ticket_levels, - :tickets, - :users - ] - FIELDS= [:id, :metadata ] + TABLES = %i[ + campaign_gift_options + campaign_gifts + campaigns + cards + charges + custom_field_masters + custom_field_joins + disputes + donations + events + imports + nonprofits + offsite_payments + payments + payment_payouts + payouts + profiles + recurring_donations + refunds + roles + supporter_emails + supporter_notes + supporters + tag_joins + tag_masters + ticket_levels + tickets + users + ].freeze + FIELDS = %i[id metadata].freeze def up - TABLES.each{|table| + TABLES.each do |table| remove_column table, :metadata - } + end end def down - TABLES.each{|table| + TABLES.each do |table| add_column table, :metadata, :text - } - + end end end diff --git a/db/migrate/20180213191755_remove_recurring_donation_event_id.rb b/db/migrate/20180213191755_remove_recurring_donation_event_id.rb index f1c207a3..b4cb776e 100644 --- a/db/migrate/20180213191755_remove_recurring_donation_event_id.rb +++ b/db/migrate/20180213191755_remove_recurring_donation_event_id.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RemoveRecurringDonationEventId < ActiveRecord::Migration def change @@ -5,5 +7,4 @@ class RemoveRecurringDonationEventId < ActiveRecord::Migration t.remove :event_id end end - end diff --git a/db/migrate/20180214124311_create_source_tokens.rb b/db/migrate/20180214124311_create_source_tokens.rb index f4d0fae3..f200799f 100644 --- a/db/migrate/20180214124311_create_source_tokens.rb +++ b/db/migrate/20180214124311_create_source_tokens.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CreateSourceTokens < ActiveRecord::Migration def change create_table :source_tokens, id: false do |t| - t.column :token, 'uuid', primary_key: true, null:false + t.column :token, 'uuid', primary_key: true, null: false t.datetime :expiration - t.references :tokenizable, :polymorphic => true + t.references :tokenizable, polymorphic: true t.references :event t.integer :max_uses, default: 1 t.integer :total_uses, default: 0 t.timestamps end - add_index :source_tokens, :token, :unique => true + add_index :source_tokens, :token, unique: true add_index :source_tokens, :expiration - add_index :source_tokens, [:tokenizable_id, :tokenizable_type] - + add_index :source_tokens, %i[tokenizable_id tokenizable_type] end end diff --git a/db/migrate/20180215124311_add_card_token_to_ticket.rb b/db/migrate/20180215124311_add_card_token_to_ticket.rb index 6a86b920..e74e40b5 100644 --- a/db/migrate/20180215124311_add_card_token_to_ticket.rb +++ b/db/migrate/20180215124311_add_card_token_to_ticket.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AddCardTokenToTicket < ActiveRecord::Migration def up diff --git a/db/migrate/20180216064311_add_indexes_to_supporter_notes.rb b/db/migrate/20180216064311_add_indexes_to_supporter_notes.rb index cc7cb1c1..e453b989 100644 --- a/db/migrate/20180216064311_add_indexes_to_supporter_notes.rb +++ b/db/migrate/20180216064311_add_indexes_to_supporter_notes.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + class AddIndexesToSupporterNotes < ActiveRecord::Migration def change - add_index :supporter_notes, :supporter_id, :order => {:supporter_id => :asc} + add_index :supporter_notes, :supporter_id, order: { supporter_id: :asc } end end diff --git a/db/migrate/20180216124311_change_ddd_supporter_to_holder.rb b/db/migrate/20180216124311_change_ddd_supporter_to_holder.rb index bb26555b..56f4cd75 100644 --- a/db/migrate/20180216124311_change_ddd_supporter_to_holder.rb +++ b/db/migrate/20180216124311_change_ddd_supporter_to_holder.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ChangeDddSupporterToHolder < ActiveRecord::Migration def change diff --git a/db/migrate/20180217124311_remove_articles.rb b/db/migrate/20180217124311_remove_articles.rb index cfe1f1da..9aae090c 100644 --- a/db/migrate/20180217124311_remove_articles.rb +++ b/db/migrate/20180217124311_remove_articles.rb @@ -1,9 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RemoveArticles < ActiveRecord::Migration def up drop_table :articles end - def down - end + def down; end end diff --git a/db/migrate/20180608205049_add_index_to_supporter_id_on_tickets.rb b/db/migrate/20180608205049_add_index_to_supporter_id_on_tickets.rb index d2dc5bf2..6dc66844 100644 --- a/db/migrate/20180608205049_add_index_to_supporter_id_on_tickets.rb +++ b/db/migrate/20180608205049_add_index_to_supporter_id_on_tickets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexToSupporterIdOnTickets < ActiveRecord::Migration def change add_index :tickets, :supporter_id diff --git a/db/migrate/20180608212658_add_index_to_event_id_on_donations_and_events.rb b/db/migrate/20180608212658_add_index_to_event_id_on_donations_and_events.rb index c86222f9..6bd038e5 100644 --- a/db/migrate/20180608212658_add_index_to_event_id_on_donations_and_events.rb +++ b/db/migrate/20180608212658_add_index_to_event_id_on_donations_and_events.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexToEventIdOnDonationsAndEvents < ActiveRecord::Migration def change add_index :tickets, :event_id diff --git a/db/migrate/20180703165401_add_parent_campaign_id_to_campaigns.rb b/db/migrate/20180703165401_add_parent_campaign_id_to_campaigns.rb index bcb567ec..725d90a2 100644 --- a/db/migrate/20180703165401_add_parent_campaign_id_to_campaigns.rb +++ b/db/migrate/20180703165401_add_parent_campaign_id_to_campaigns.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddParentCampaignIdToCampaigns < ActiveRecord::Migration def change add_column :campaigns, :parent_campaign_id, :integer diff --git a/db/migrate/20180703165402_add_reason_for_supporting_to_campaigns.rb b/db/migrate/20180703165402_add_reason_for_supporting_to_campaigns.rb index 2f6791ca..2d447482 100644 --- a/db/migrate/20180703165402_add_reason_for_supporting_to_campaigns.rb +++ b/db/migrate/20180703165402_add_reason_for_supporting_to_campaigns.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddReasonForSupportingToCampaigns < ActiveRecord::Migration def change add_column :campaigns, :reason_for_supporting, :text diff --git a/db/migrate/20180703165404_add_default_reason_for_supporting_to_campaigns.rb b/db/migrate/20180703165404_add_default_reason_for_supporting_to_campaigns.rb index 521ade97..8cc3ddeb 100644 --- a/db/migrate/20180703165404_add_default_reason_for_supporting_to_campaigns.rb +++ b/db/migrate/20180703165404_add_default_reason_for_supporting_to_campaigns.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDefaultReasonForSupportingToCampaigns < ActiveRecord::Migration def change add_column :campaigns, :default_reason_for_supporting, :text diff --git a/db/migrate/20180703165405_add_banner_image_to_campaigns.rb b/db/migrate/20180703165405_add_banner_image_to_campaigns.rb index 8be6b2a9..9bedf1fd 100644 --- a/db/migrate/20180703165405_add_banner_image_to_campaigns.rb +++ b/db/migrate/20180703165405_add_banner_image_to_campaigns.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddBannerImageToCampaigns < ActiveRecord::Migration def change add_column :campaigns, :banner_image, :string diff --git a/db/migrate/20180713213748_add_charge_id_indexes.rb b/db/migrate/20180713213748_add_charge_id_indexes.rb index 52d50a5e..258f1646 100644 --- a/db/migrate/20180713213748_add_charge_id_indexes.rb +++ b/db/migrate/20180713213748_add_charge_id_indexes.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + class AddChargeIdIndexes < ActiveRecord::Migration def change add_index :refunds, :charge_id end - end diff --git a/db/migrate/20180713215825_add_payment_id_to_tickets.rb b/db/migrate/20180713215825_add_payment_id_to_tickets.rb index bbfb275c..dfaf0041 100644 --- a/db/migrate/20180713215825_add_payment_id_to_tickets.rb +++ b/db/migrate/20180713215825_add_payment_id_to_tickets.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddPaymentIdToTickets < ActiveRecord::Migration def change add_index :tickets, :payment_id diff --git a/db/migrate/20180713220028_add_indexes_to_refunds.rb b/db/migrate/20180713220028_add_indexes_to_refunds.rb index f618006b..6f215d02 100644 --- a/db/migrate/20180713220028_add_indexes_to_refunds.rb +++ b/db/migrate/20180713220028_add_indexes_to_refunds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexesToRefunds < ActiveRecord::Migration def change add_index :refunds, :payment_id diff --git a/db/migrate/20181002160627_correct_dedications.rb b/db/migrate/20181002160627_correct_dedications.rb index 40657773..a590b189 100644 --- a/db/migrate/20181002160627_correct_dedications.rb +++ b/db/migrate/20181002160627_correct_dedications.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + class CorrectDedications < ActiveRecord::Migration def up - execute <<-SQL - create or replace function is_valid_json(p_json text) - returns boolean -as -$$ -begin - return (p_json::json is not null); -exception - when others then - return false; -end; -$$ -language plpgsql -immutable; + execute <<~SQL + create or replace function is_valid_json(p_json text) + returns boolean + as + $$ + begin + return (p_json::json is not null); + exception + when others then + return false; + end; + $$ + language plpgsql + immutable; SQL dedications = MaintainDedications.retrieve_non_json_dedications @@ -24,7 +26,5 @@ immutable; MaintainDedications.add_honor_to_any_json_dedications_without_type(dedications) end - def down - end + def down; end end - diff --git a/db/migrate/20181003212559_correct_dedication_contacts.rb b/db/migrate/20181003212559_correct_dedication_contacts.rb index 9631d9ba..a61a68ff 100644 --- a/db/migrate/20181003212559_correct_dedication_contacts.rb +++ b/db/migrate/20181003212559_correct_dedication_contacts.rb @@ -1,33 +1,36 @@ +# frozen_string_literal: true + class CorrectDedicationContacts < ActiveRecord::Migration def up json_dedications = Qx.select('id', 'dedication').from(:donations) - .where("dedication IS NOT NULL AND dedication != ''") - .and_where("is_valid_json(dedication)").ex - parsed_dedications = json_dedications.map{|i| {id: i['id'], dedication: JSON.parse(i['dedication'])}} - with_contact_to_correct = parsed_dedications.select {|i| !i[:dedication]['contact'].blank? && i[:dedication]['contact'].is_a?(String)} - really_icky_dedications, easy_to_split_strings = with_contact_to_correct.partition{|i| i[:dedication]['contact'].split(" - ").count > 3} + .where("dedication IS NOT NULL AND dedication != ''") + .and_where('is_valid_json(dedication)').ex + parsed_dedications = json_dedications.map { |i| { id: i['id'], dedication: JSON.parse(i['dedication']) } } + with_contact_to_correct = parsed_dedications.select { |i| !i[:dedication]['contact'].blank? && i[:dedication]['contact'].is_a?(String) } + really_icky_dedications, easy_to_split_strings = with_contact_to_correct.partition { |i| i[:dedication]['contact'].split(' - ').count > 3 } - easy_to_split_strings.map do |i| - split_contact = i[:dedication]['contact'].split(' - ') - i[:dedication]['contact'] = { - email: split_contact[0], - phone:split_contact[1], - address: split_contact[2], - } - puts i - i - end.each_with_index do |i, index| - unless (i[:id]) - raise Error("Item at index #{index} is invalid. Object:#{i}") - end - Qx.update(:donations).where("id = $id", id:i[:id]).set(dedication: JSON.generate(i[:dedication])).ex - end + easy_to_split_strings.map do |i| + split_contact = i[:dedication]['contact'].split(' - ') + i[:dedication]['contact'] = { + email: split_contact[0], + phone: split_contact[1], + address: split_contact[2] + } + puts i + i + end.each_with_index do |i, index| + unless i[:id] + raise Error("Item at index #{index} is invalid. Object:#{i}") + end + + Qx.update(:donations).where('id = $id', id: i[:id]).set(dedication: JSON.generate(i[:dedication])).ex + end puts "Corrected #{easy_to_split_strings.count} records." - puts "" - puts "" - puts "You must manually fix the following dedications: " + puts '' + puts '' + puts 'You must manually fix the following dedications: ' really_icky_dedications.each do |i| puts i end @@ -35,18 +38,18 @@ class CorrectDedicationContacts < ActiveRecord::Migration def down json_dedications = Qx.select('id', 'dedication').from(:donations) - .where("dedication IS NOT NULL AND dedication != ''") - .and_where("is_valid_json(dedication)").ex + .where("dedication IS NOT NULL AND dedication != ''") + .and_where('is_valid_json(dedication)').ex - parsed_dedications = json_dedications.map{|i| {'id' => i['id'], 'dedication' => JSON.parse(i['dedication'])}} + parsed_dedications = json_dedications.map { |i| { 'id' => i['id'], 'dedication' => JSON.parse(i['dedication']) } } - with_contact_to_correct = parsed_dedications.select {|i| i['dedication']['contact'].is_a?(Hash)} + with_contact_to_correct = parsed_dedications.select { |i| i['dedication']['contact'].is_a?(Hash) } puts "#{with_contact_to_correct.count} to revert" with_contact_to_correct.each do |i| contact_string = "#{i['dedication']['contact']['email']} - #{i['dedication']['contact']['phone']} - #{i['dedication']['contact']['address']}" i['dedication']['contact'] = contact_string - Qx.update(:donations).where("id = $id", id:i['id']).set(dedication: JSON.generate(i['dedication'])).ex + Qx.update(:donations).where('id = $id', id: i['id']).set(dedication: JSON.generate(i['dedication'])).ex end end end diff --git a/db/migrate/20181120182105_add_index_parent_campaign_id_to_campaign.rb b/db/migrate/20181120182105_add_index_parent_campaign_id_to_campaign.rb index 81353b42..7d620860 100644 --- a/db/migrate/20181120182105_add_index_parent_campaign_id_to_campaign.rb +++ b/db/migrate/20181120182105_add_index_parent_campaign_id_to_campaign.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexParentCampaignIdToCampaign < ActiveRecord::Migration def change add_index :campaigns, :parent_campaign_id diff --git a/db/migrate/20181128221143_add_indexes_to_recurring_donations.rb b/db/migrate/20181128221143_add_indexes_to_recurring_donations.rb index 9bdb1c5f..7fec2af3 100644 --- a/db/migrate/20181128221143_add_indexes_to_recurring_donations.rb +++ b/db/migrate/20181128221143_add_indexes_to_recurring_donations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexesToRecurringDonations < ActiveRecord::Migration def change add_index :recurring_donations, :donation_id diff --git a/db/migrate/20181129205652_add_donation_id_index_to_campaign_gifts.rb b/db/migrate/20181129205652_add_donation_id_index_to_campaign_gifts.rb index 8ead22c7..eb79be63 100644 --- a/db/migrate/20181129205652_add_donation_id_index_to_campaign_gifts.rb +++ b/db/migrate/20181129205652_add_donation_id_index_to_campaign_gifts.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddDonationIdIndexToCampaignGifts < ActiveRecord::Migration def change add_index :campaign_gifts, :donation_id diff --git a/db/migrate/20181129224030_add_index_to_payments_created_at.rb b/db/migrate/20181129224030_add_index_to_payments_created_at.rb index b1a6877c..a915cbca 100644 --- a/db/migrate/20181129224030_add_index_to_payments_created_at.rb +++ b/db/migrate/20181129224030_add_index_to_payments_created_at.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class AddIndexToPaymentsCreatedAt < ActiveRecord::Migration def change add_index :payments, :created_at diff --git a/db/seeds.rb b/db/seeds.rb index 0855c107..8ce782cc 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later bp = BillingPlan.new -bp.name = "Default billing plan" +bp.name = 'Default billing plan' bp.amount = 0 bp.percentage_fee = 0 -bp.save! \ No newline at end of file +bp.save! diff --git a/gems/grape_devise/Gemfile b/gems/grape_devise/Gemfile index bd48deef..1a91d437 100644 --- a/gems/grape_devise/Gemfile +++ b/gems/grape_devise/Gemfile @@ -1,4 +1,6 @@ -source "https://rubygems.org" +# frozen_string_literal: true + +source 'https://rubygems.org' # Declare your gem's dependencies in grape_devise.gemspec. # Bundler will treat runtime dependencies like base dependencies, and diff --git a/gems/grape_devise/Rakefile b/gems/grape_devise/Rakefile index c03dcec3..07595d91 100644 --- a/gems/grape_devise/Rakefile +++ b/gems/grape_devise/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + begin require 'bundler/setup' rescue LoadError @@ -18,8 +20,6 @@ require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) - Bundler::GemHelper.install_tasks - task default: :spec diff --git a/gems/grape_devise/grape_devise.gemspec b/gems/grape_devise/grape_devise.gemspec index 285e4bca..6cf5fa2f 100644 --- a/gems/grape_devise/grape_devise.gemspec +++ b/gems/grape_devise/grape_devise.gemspec @@ -1,32 +1,34 @@ -$:.push File.expand_path("../lib", __FILE__) +# frozen_string_literal: true + +$LOAD_PATH.push File.expand_path('lib', __dir__) # Maintain your gem's version: -require "grape_devise/version" +require 'grape_devise/version' # Describe your gem and declare its dependencies: Gem::Specification.new do |s| - s.name = "grape_devise" + s.name = 'grape_devise' s.version = GrapeDevise::VERSION - s.authors = ["Justin McCormick"] - s.email = ["me@justinmccormick.com"] - s.homepage = "http://github.com/justinm/grape_devise" - s.summary = "Adds support for devise in grape applications" - s.description = "This gem provides access to devise helpers inside of " - "Grape applications." + s.authors = ['Justin McCormick'] + s.email = ['me@justinmccormick.com'] + s.homepage = 'http://github.com/justinm/grape_devise' + s.summary = 'Adds support for devise in grape applications' + s.description = 'This gem provides access to devise helpers inside of ' + 'Grape applications.' - s.licenses = [ 'MIT' ] + s.licenses = ['MIT'] - s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE ", "Rakefile", "README.rdoc"] - s.test_files = Dir["test/**/*"] + s.files = Dir['{app,config,db,lib}/**/*', 'MIT-LICENSE ', 'Rakefile', 'README.rdoc'] + s.test_files = Dir['test/**/*'] - s.add_development_dependency "rspec", "~> 2.99.0" - s.add_development_dependency "rspec-rails", "~> 2.99.0" - s.add_development_dependency "capybara", "~> 2.4.1" - s.add_development_dependency "factory_girl", "~> 4.4.0" - s.add_development_dependency "activerecord-nulldb-adapter", "~> 0.3.1" - s.add_development_dependency "sqlite3" + s.add_development_dependency 'activerecord-nulldb-adapter', '~> 0.3.1' + s.add_development_dependency 'capybara', '~> 2.4.1' + s.add_development_dependency 'factory_girl', '~> 4.4.0' + s.add_development_dependency 'rspec', '~> 2.99.0' + s.add_development_dependency 'rspec-rails', '~> 2.99.0' + s.add_development_dependency 'sqlite3' - s.add_dependency "devise", ">= 2.2.8", "< 5" - s.add_dependency "grape", "> 0.7" - s.add_dependency "rails", "> 3.2", "< 6" + s.add_dependency 'devise', '>= 2.2.8', '< 5' + s.add_dependency 'grape', '> 0.7' + s.add_dependency 'rails', '> 3.2', '< 6' end diff --git a/gems/grape_devise/lib/grape_devise.rb b/gems/grape_devise/lib/grape_devise.rb index e6e9c268..533cc787 100644 --- a/gems/grape_devise/lib/grape_devise.rb +++ b/gems/grape_devise/lib/grape_devise.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module GrapeDevise end -require "devise" -require "grape" -require "grape_devise/api" +require 'devise' +require 'grape' +require 'grape_devise/api' Devise.helpers << GrapeDevise::API Grape::Endpoint.send :include, GrapeDevise::API diff --git a/gems/grape_devise/lib/grape_devise/api.rb b/gems/grape_devise/lib/grape_devise/api.rb index 9658cda8..771d69f7 100644 --- a/gems/grape_devise/lib/grape_devise/api.rb +++ b/gems/grape_devise/lib/grape_devise/api.rb @@ -1,10 +1,12 @@ +# frozen_string_literal: true + require 'devise' module GrapeDevise::API extend ActiveSupport::Concern include Devise::Controllers::SignInOut - - def self.define_helpers mapping + + def self.define_helpers(mapping) mapping = mapping.name.to_s class_eval <<-METHODS, __FILE__, __LINE__ + 1 @@ -33,5 +35,4 @@ module GrapeDevise::API end METHODS end - end diff --git a/gems/grape_devise/lib/grape_devise/version.rb b/gems/grape_devise/lib/grape_devise/version.rb index 199dff0a..0932866c 100644 --- a/gems/grape_devise/lib/grape_devise/version.rb +++ b/gems/grape_devise/lib/grape_devise/version.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module GrapeDevise - VERSION = "0.1.1" + VERSION = '0.1.1' end diff --git a/gems/grape_devise/spec/dummy/Rakefile b/gems/grape_devise/spec/dummy/Rakefile index eb4e20b0..5df4030a 100644 --- a/gems/grape_devise/spec/dummy/Rakefile +++ b/gems/grape_devise/spec/dummy/Rakefile @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # Add your own tasks in files placed in lib/tasks ending in .rake, # for example lib/tasks/capistrano.rake, and they will automatically be available to Rake. -require File.expand_path('../config/application', __FILE__) +require File.expand_path('config/application', __dir__) require 'rspec/core/rake_task' RSpec::Core::RakeTask.new(:spec) diff --git a/gems/grape_devise/spec/dummy/app/api/api.rb b/gems/grape_devise/spec/dummy/app/api/api.rb index e5e2cc4e..b341fc5d 100644 --- a/gems/grape_devise/spec/dummy/app/api/api.rb +++ b/gems/grape_devise/spec/dummy/app/api/api.rb @@ -1,18 +1,20 @@ -require "grape_devise" +# frozen_string_literal: true + +require 'grape_devise' class API < Grape::API format :json - get "me" do + get 'me' do authenticate_user! current_user end - get "authorized" do + get 'authorized' do user_signed_in? end - post "signin" do + post 'signin' do authenticate_user! end -end \ No newline at end of file +end diff --git a/gems/grape_devise/spec/dummy/app/controllers/application_controller.rb b/gems/grape_devise/spec/dummy/app/controllers/application_controller.rb index d83690e1..1ff0944d 100644 --- a/gems/grape_devise/spec/dummy/app/controllers/application_controller.rb +++ b/gems/grape_devise/spec/dummy/app/controllers/application_controller.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class ApplicationController < ActionController::Base # Prevent CSRF attacks by raising an exception. # For APIs, you may want to use :null_session instead. diff --git a/gems/grape_devise/spec/dummy/app/helpers/application_helper.rb b/gems/grape_devise/spec/dummy/app/helpers/application_helper.rb index de6be794..15b06f0f 100644 --- a/gems/grape_devise/spec/dummy/app/helpers/application_helper.rb +++ b/gems/grape_devise/spec/dummy/app/helpers/application_helper.rb @@ -1,2 +1,4 @@ +# frozen_string_literal: true + module ApplicationHelper end diff --git a/gems/grape_devise/spec/dummy/app/models/user.rb b/gems/grape_devise/spec/dummy/app/models/user.rb index a103d300..e6a02e67 100644 --- a/gems/grape_devise/spec/dummy/app/models/user.rb +++ b/gems/grape_devise/spec/dummy/app/models/user.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + class User < ActiveRecord::Base # Include default devise modules. Others available are: # :confirmable, :lockable, :timeoutable and :omniauthable diff --git a/gems/grape_devise/spec/dummy/bin/bundle b/gems/grape_devise/spec/dummy/bin/bundle index 66e9889e..2dbb7176 100755 --- a/gems/grape_devise/spec/dummy/bin/bundle +++ b/gems/grape_devise/spec/dummy/bin/bundle @@ -1,3 +1,5 @@ #!/usr/bin/env ruby -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__) +# frozen_string_literal: true + +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) load Gem.bin_path('bundler', 'bundle') diff --git a/gems/grape_devise/spec/dummy/bin/rails b/gems/grape_devise/spec/dummy/bin/rails index 728cd85a..a31728ab 100755 --- a/gems/grape_devise/spec/dummy/bin/rails +++ b/gems/grape_devise/spec/dummy/bin/rails @@ -1,4 +1,6 @@ #!/usr/bin/env ruby -APP_PATH = File.expand_path('../../config/application', __FILE__) +# frozen_string_literal: true + +APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' require 'rails/commands' diff --git a/gems/grape_devise/spec/dummy/bin/rake b/gems/grape_devise/spec/dummy/bin/rake index 17240489..c1999550 100755 --- a/gems/grape_devise/spec/dummy/bin/rake +++ b/gems/grape_devise/spec/dummy/bin/rake @@ -1,4 +1,6 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + require_relative '../config/boot' require 'rake' Rake.application.run diff --git a/gems/grape_devise/spec/dummy/config.ru b/gems/grape_devise/spec/dummy/config.ru index 5bc2a619..61c04e13 100644 --- a/gems/grape_devise/spec/dummy/config.ru +++ b/gems/grape_devise/spec/dummy/config.ru @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # This file is used by Rack-based servers to start the application. -require ::File.expand_path('../config/environment', __FILE__) +require ::File.expand_path('../config/environment', __FILE__) run Rails.application diff --git a/gems/grape_devise/spec/dummy/config/application.rb b/gems/grape_devise/spec/dummy/config/application.rb index 2d56f86a..d985cb27 100644 --- a/gems/grape_devise/spec/dummy/config/application.rb +++ b/gems/grape_devise/spec/dummy/config/application.rb @@ -1,7 +1,9 @@ -require File.expand_path('../boot', __FILE__) +# frozen_string_literal: true + +require File.expand_path('boot', __dir__) require 'rails/all' -#require '../..app/models/user' +# require '../..app/models/user' Bundler.require(*Rails.groups) @@ -11,4 +13,3 @@ module Dummy config.autoload_paths += Dir[Rails.root.join('app', 'api', '*')] end end - diff --git a/gems/grape_devise/spec/dummy/config/boot.rb b/gems/grape_devise/spec/dummy/config/boot.rb index ef360470..6d2cba07 100644 --- a/gems/grape_devise/spec/dummy/config/boot.rb +++ b/gems/grape_devise/spec/dummy/config/boot.rb @@ -1,5 +1,7 @@ -# Set up gems listed in the Gemfile. -ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__) +# frozen_string_literal: true -require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) -$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__) +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../Gemfile', __dir__) + +require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE']) +$LOAD_PATH.unshift File.expand_path('../../../lib', __dir__) diff --git a/gems/grape_devise/spec/dummy/config/environment.rb b/gems/grape_devise/spec/dummy/config/environment.rb index 10e0cadc..bdab7759 100644 --- a/gems/grape_devise/spec/dummy/config/environment.rb +++ b/gems/grape_devise/spec/dummy/config/environment.rb @@ -1,5 +1,7 @@ +# frozen_string_literal: true + # Load the Rails application. -require File.expand_path('../application', __FILE__) +require File.expand_path('application', __dir__) # Initialize the Rails application. Dummy::Application.initialize! diff --git a/gems/grape_devise/spec/dummy/config/environments/development.rb b/gems/grape_devise/spec/dummy/config/environments/development.rb index 9d26e125..576e6795 100644 --- a/gems/grape_devise/spec/dummy/config/environments/development.rb +++ b/gems/grape_devise/spec/dummy/config/environments/development.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/gems/grape_devise/spec/dummy/config/environments/production.rb b/gems/grape_devise/spec/dummy/config/environments/production.rb index b690b1cf..a7d2e602 100644 --- a/gems/grape_devise/spec/dummy/config/environments/production.rb +++ b/gems/grape_devise/spec/dummy/config/environments/production.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb. diff --git a/gems/grape_devise/spec/dummy/config/environments/test.rb b/gems/grape_devise/spec/dummy/config/environments/test.rb index aa7c6159..8ad50deb 100644 --- a/gems/grape_devise/spec/dummy/config/environments/test.rb +++ b/gems/grape_devise/spec/dummy/config/environments/test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Dummy::Application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -14,7 +16,7 @@ Dummy::Application.configure do # Configure static asset server for tests with Cache-Control for performance. config.serve_static_assets = true - config.static_cache_control = "public, max-age=3600" + config.static_cache_control = 'public, max-age=3600' # Show full error reports and disable caching. config.consider_all_requests_local = true diff --git a/gems/grape_devise/spec/dummy/config/initializers/backtrace_silencers.rb b/gems/grape_devise/spec/dummy/config/initializers/backtrace_silencers.rb index 59385cdf..4b63f289 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/backtrace_silencers.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/backtrace_silencers.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # You can add backtrace silencers for libraries that you're using but don't wish to see in your backtraces. diff --git a/gems/grape_devise/spec/dummy/config/initializers/devise.rb b/gems/grape_devise/spec/dummy/config/initializers/devise.rb index a9cbda54..dbbfbded 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/devise.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/devise.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'devise' # Use this hook to configure devise mailer, warden hooks and so forth. # Many of these configuration options can be set straight in your model. @@ -42,12 +44,12 @@ Devise.setup do |config| # Configure which authentication keys should be case-insensitive. # These keys will be downcased upon creating or modifying a user and when used # to authenticate or find a user. Default is :email. - config.case_insensitive_keys = [ :email ] + config.case_insensitive_keys = [:email] # Configure which authentication keys should have whitespace stripped. # These keys will have whitespace before and after removed upon creating or # modifying a user and when used to authenticate or find a user. Default is :email. - config.strip_whitespace_keys = [ :email ] + config.strip_whitespace_keys = [:email] # Tell if authentication through request.params is enabled. True by default. # It can be set to an array that will enable params authentication only for the diff --git a/gems/grape_devise/spec/dummy/config/initializers/filter_parameter_logging.rb b/gems/grape_devise/spec/dummy/config/initializers/filter_parameter_logging.rb index 4a994e1e..7a4f47b4 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/filter_parameter_logging.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/filter_parameter_logging.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Configure sensitive parameters which will be filtered from the log file. diff --git a/gems/grape_devise/spec/dummy/config/initializers/inflections.rb b/gems/grape_devise/spec/dummy/config/initializers/inflections.rb index ac033bf9..dc847422 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/inflections.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/inflections.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new inflection rules using the following format. Inflections diff --git a/gems/grape_devise/spec/dummy/config/initializers/mime_types.rb b/gems/grape_devise/spec/dummy/config/initializers/mime_types.rb index 72aca7e4..df5ec138 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/mime_types.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/mime_types.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Add new mime types for use in respond_to blocks: diff --git a/gems/grape_devise/spec/dummy/config/initializers/secret_token.rb b/gems/grape_devise/spec/dummy/config/initializers/secret_token.rb index 823ac2dd..ec8d472b 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/secret_token.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/secret_token.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # Your secret key is used for verifying the integrity of signed cookies. diff --git a/gems/grape_devise/spec/dummy/config/initializers/session_store.rb b/gems/grape_devise/spec/dummy/config/initializers/session_store.rb index 155f7b02..ba27628a 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/session_store.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/session_store.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. Dummy::Application.config.session_store :cookie_store, key: '_dummy_session' diff --git a/gems/grape_devise/spec/dummy/config/initializers/wrap_parameters.rb b/gems/grape_devise/spec/dummy/config/initializers/wrap_parameters.rb index 33725e95..246168a4 100644 --- a/gems/grape_devise/spec/dummy/config/initializers/wrap_parameters.rb +++ b/gems/grape_devise/spec/dummy/config/initializers/wrap_parameters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # Be sure to restart your server when you modify this file. # This file contains settings for ActionController::ParamsWrapper which diff --git a/gems/grape_devise/spec/dummy/config/routes.rb b/gems/grape_devise/spec/dummy/config/routes.rb index be2b51d2..ed698740 100644 --- a/gems/grape_devise/spec/dummy/config/routes.rb +++ b/gems/grape_devise/spec/dummy/config/routes.rb @@ -1,7 +1,8 @@ -require "devise" +# frozen_string_literal: true + +require 'devise' Dummy::Application.routes.draw do - devise_for :users mount API => '/' end diff --git a/gems/grape_devise/spec/dummy/db/migrate/20140913043018_devise_create_users.rb b/gems/grape_devise/spec/dummy/db/migrate/20140913043018_devise_create_users.rb index cf497c27..620b04c3 100644 --- a/gems/grape_devise/spec/dummy/db/migrate/20140913043018_devise_create_users.rb +++ b/gems/grape_devise/spec/dummy/db/migrate/20140913043018_devise_create_users.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + class DeviseCreateUsers < ActiveRecord::Migration def change create_table(:users) do |t| ## Database authenticatable - t.string :email, null: false, default: "" - t.string :encrypted_password, null: false, default: "" + t.string :email, null: false, default: '' + t.string :encrypted_password, null: false, default: '' ## Recoverable t.string :reset_password_token @@ -30,7 +32,6 @@ class DeviseCreateUsers < ActiveRecord::Migration # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at - t.timestamps end diff --git a/gems/grape_devise/spec/dummy/db/schema.rb b/gems/grape_devise/spec/dummy/db/schema.rb index 905d99e3..2f5ad323 100644 --- a/gems/grape_devise/spec/dummy/db/schema.rb +++ b/gems/grape_devise/spec/dummy/db/schema.rb @@ -1,4 +1,5 @@ -# encoding: UTF-8 +# frozen_string_literal: true + # This file is auto-generated from the current state of the database. Instead # of editing this file, please use the migrations feature of Active Record to # incrementally modify your database, and then regenerate this schema definition. @@ -11,24 +12,22 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema.define(version: 20140913043018) do - - create_table "users", force: true do |t| - t.string "email", default: "", null: false - t.string "encrypted_password", default: "", null: false - t.string "reset_password_token" - t.datetime "reset_password_sent_at" - t.datetime "remember_created_at" - t.integer "sign_in_count", default: 0, null: false - t.datetime "current_sign_in_at" - t.datetime "last_sign_in_at" - t.string "current_sign_in_ip" - t.string "last_sign_in_ip" - t.datetime "created_at" - t.datetime "updated_at" +ActiveRecord::Schema.define(version: 20_140_913_043_018) do + create_table 'users', force: true do |t| + t.string 'email', default: '', null: false + t.string 'encrypted_password', default: '', null: false + t.string 'reset_password_token' + t.datetime 'reset_password_sent_at' + t.datetime 'remember_created_at' + t.integer 'sign_in_count', default: 0, null: false + t.datetime 'current_sign_in_at' + t.datetime 'last_sign_in_at' + t.string 'current_sign_in_ip' + t.string 'last_sign_in_ip' + t.datetime 'created_at' + t.datetime 'updated_at' end - add_index "users", ["email"], name: "index_users_on_email", unique: true - add_index "users", ["reset_password_token"], name: "index_users_on_reset_password_token", unique: true - + add_index 'users', ['email'], name: 'index_users_on_email', unique: true + add_index 'users', ['reset_password_token'], name: 'index_users_on_reset_password_token', unique: true end diff --git a/gems/grape_devise/spec/factories.rb b/gems/grape_devise/spec/factories.rb index beb87070..a3975f3a 100644 --- a/gems/grape_devise/spec/factories.rb +++ b/gems/grape_devise/spec/factories.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + FactoryGirl.define do factory :user do - email "john.doe@example.com" - password "12345" - password_confirmation "12345" + email 'john.doe@example.com' + password '12345' + password_confirmation '12345' end end diff --git a/gems/grape_devise/spec/requests/user_spec.rb b/gems/grape_devise/spec/requests/user_spec.rb index 86bc519e..25ce81bd 100644 --- a/gems/grape_devise/spec/requests/user_spec.rb +++ b/gems/grape_devise/spec/requests/user_spec.rb @@ -1,52 +1,53 @@ +# frozen_string_literal: true + require 'spec_helper' require 'warden/test/helpers' -RSpec.describe API, :type => :request do +RSpec.describe API, type: :request do include Warden::Test::Helpers let(:user) { build(:user) } - after{ Warden.test_reset! } + after { Warden.test_reset! } - it "should return the current user" do + it 'should return the current user' do login_as user, scope: :user - get "/me" + get '/me' response.body.should eq(user.to_json) end - it "should return an error if not logged in" do + it 'should return an error if not logged in' do login_as nil, scope: :user - get "/me" + get '/me' - response.code.should eq("401") + response.code.should eq('401') end - it "should return true if logged in" do + it 'should return true if logged in' do login_as user, scope: :user - get "/authorized" + get '/authorized' - response.body.should eq("true") + response.body.should eq('true') end - it "should return false if logged out" do + it 'should return false if logged out' do login_as nil, scope: :user - get "/authorized" + get '/authorized' - response.body.should eq("false") + response.body.should eq('false') end - it "should log in the user" do + it 'should log in the user' do User.stub :find_for_database_authentication do user end - post "/signin", { user: { email: user.email, password: user.password } } + post '/signin', user: { email: user.email, password: user.password } - response.code.should eq("201") + response.code.should eq('201') end - -end \ No newline at end of file +end diff --git a/gems/grape_devise/spec/spec_helper.rb b/gems/grape_devise/spec/spec_helper.rb index 89343208..0212f7b1 100644 --- a/gems/grape_devise/spec/spec_helper.rb +++ b/gems/grape_devise/spec/spec_helper.rb @@ -1,7 +1,9 @@ -# Configure Rails Environment -ENV["RAILS_ENV"] = "test" +# frozen_string_literal: true -require File.expand_path("../dummy/config/environment.rb", __FILE__) +# Configure Rails Environment +ENV['RAILS_ENV'] = 'test' + +require File.expand_path('dummy/config/environment.rb', __dir__) require 'rspec' require 'rspec/rails' require 'factory_girl' @@ -16,7 +18,7 @@ Rails.backtrace_cleaner.remove_silencers! # Requires supporting ruby files with custom matchers and macros, etc, # in spec/support/ and its subdirectories. -Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f} +Dir[Rails.root.join('spec/support/**/*.rb')].each { |f| require f } RSpec.configure do |config| # == Mock Framework diff --git a/gems/ruby-param-validation/Rakefile b/gems/ruby-param-validation/Rakefile index 1f33d053..dc5d5818 100644 --- a/gems/ruby-param-validation/Rakefile +++ b/gems/ruby-param-validation/Rakefile @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'rake/testtask' Rake::TestTask.new do |t| @@ -5,5 +7,5 @@ Rake::TestTask.new do |t| t.test_files = FileList['test/test*.rb', 'test/*test.rb'] end -desc "Run tests" -task :default => :test \ No newline at end of file +desc 'Run tests' +task default: :test diff --git a/gems/ruby-param-validation/lib/param_validation.rb b/gems/ruby-param-validation/lib/param_validation.rb index 131ffc31..2835763d 100644 --- a/gems/ruby-param-validation/lib/param_validation.rb +++ b/gems/ruby-param-validation/lib/param_validation.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + require 'json' require 'chronic' class ParamValidation - # Given a hash of data and a validation hash, check all the validations, raising an Error on the first invalid key # @raise [ValidationError] if one or more of the validations fail. def initialize(data, validations) @@ -10,34 +11,46 @@ class ParamValidation validations.each do |key, validators| val = key === :root ? data : (data[key] || data[key.to_s] || data[key.to_sym]) next if validators[:required].nil? && val.nil? + validators.each do |name, arg| validator = @@validators[name] msg = validations[key][:message] next unless validator + is_valid = @@validators[name].call(val, arg, data) msg_proc = @@messages[name] - msg ||= @@messages[name].call({key: key, data: data, val: val, arg: arg}) if msg_proc - errors.push({msg: msg, data: {key: key, val: val, name: name, msg: msg}}) unless is_valid + msg ||= @@messages[name].call(key: key, data: data, val: val, arg: arg) if msg_proc + errors.push(msg: msg, data: { key: key, val: val, name: name, msg: msg }) unless is_valid end end if errors.length == 1 raise ValidationError.new(errors[0][:msg], errors[0][:data]) elsif errors.length > 1 - msg = errors.collect {|e| e[:msg]}.join('\n') - raise ValidationError.new(msg, errors.collect{|e| e[:data]}) + msg = errors.collect { |e| e[:msg] }.join('\n') + raise ValidationError.new(msg, errors.collect { |e| e[:data] }) end end - def self.messages; @@messages; end + def self.messages + @@messages + end + def self.set_message(name, &block) @@messages[name] = block end - def self.validators; @@validators; end + def self.validators + @@validators + end + def self.add_validator(name, &block) @@validators[name] = block end - def self.structure_validators; @@structure_validators; end + + def self.structure_validators + @@structure_validators + end + def self.add_structure_validator(name, &block) @@structure_validators[name] = block end @@ -47,64 +60,122 @@ class ParamValidation # - arg is the argument passed into the validator (eg for {required: true}, it is `true`) # - data is the entire set of data @@validators = { - required: lambda {|val, arg, data| !val.nil?}, - absent: lambda {|val, arg, data| val.nil?}, - not_blank: lambda {|val, arg, data| val.is_a?(String) && val.length > 0}, - not_included_in: lambda {|val, arg, data| !arg.include?(val) rescue false}, - included_in: lambda {|val, arg, data| arg.include?(val) rescue false}, - format: lambda {|val, arg, data| val =~ arg rescue false}, - is_integer: lambda {|val, arg, data| val.is_a?(Integer) || val =~ /\A[+-]?\d+\Z/}, - is_float: lambda {|val, arg, data| val.is_a?(Float) || (!!Float(val) rescue false) }, - min_length: lambda {|val, arg, data| val.length >= arg rescue false}, - max_length: lambda {|val, arg, data| val.length <= arg rescue false}, - length_range: lambda {|val, arg, data| arg.cover?(val.length) rescue false}, - length_equals: lambda {|val, arg, data| val.length == arg}, - is_reference: lambda{|val, arg, data| (val.is_a?(Integer)&& val >=0) || val =~ /\A\d+\Z/ || val == ''}, - equals: lambda {|val, arg, data| val == arg}, - min: lambda {|val, arg, data| val >= arg rescue false}, - max: lambda {|val, arg, data| val <= arg rescue false}, - is_array: lambda {|val, arg, data| val.is_a?(Array)}, - is_hash: lambda {|val, arg, data| val.is_a?(Hash)}, - is_json: lambda {|val, arg, data| ParamValidation.is_valid_json?(val)}, - in_range: lambda {|val, arg, data| arg.cover?(val) rescue false}, - is_a: lambda {|val, arg, data| arg.kind_of?(Enumerable) ? arg.any? {|i| val.is_a?(i)} : val.is_a?(arg)}, - can_be_date: lambda {|val, arg, data| val.is_a?(Date) || val.is_a?(DateTime) || Chronic.parse(val)}, - array_of_hashes: lambda {|val, arg, data| data.is_a?(Array) && data.map{|pair| ParamValidation.new(pair.to_h, arg)}.all?} + required: ->(val, _arg, _data) { !val.nil? }, + absent: ->(val, _arg, _data) { val.nil? }, + not_blank: ->(val, _arg, _data) { val.is_a?(String) && !val.empty? }, + not_included_in: lambda { |val, arg, _data| + begin + !arg.include?(val) + rescue StandardError + false + end + }, + included_in: lambda { |val, arg, _data| + begin + arg.include?(val) + rescue StandardError + false + end + }, + format: lambda { |val, arg, _data| + begin + val =~ arg + rescue StandardError + false + end + }, + is_integer: ->(val, _arg, _data) { val.is_a?(Integer) || val =~ /\A[+-]?\d+\Z/ }, + is_float: lambda { |val, _arg, _data| + val.is_a?(Float) || (begin + !!Float(val) + rescue StandardError + false + end) + }, + min_length: lambda { |val, arg, _data| + begin + val.length >= arg + rescue StandardError + false + end + }, + max_length: lambda { |val, arg, _data| + begin + val.length <= arg + rescue StandardError + false + end + }, + length_range: lambda { |val, arg, _data| + begin + arg.cover?(val.length) + rescue StandardError + false + end + }, + length_equals: ->(val, arg, _data) { val.length == arg }, + is_reference: ->(val, _arg, _data) { (val.is_a?(Integer) && val >= 0) || val =~ /\A\d+\Z/ || val == '' }, + equals: ->(val, arg, _data) { val == arg }, + min: lambda { |val, arg, _data| + begin + val >= arg + rescue StandardError + false + end + }, + max: lambda { |val, arg, _data| + begin + val <= arg + rescue StandardError + false + end + }, + is_array: ->(val, _arg, _data) { val.is_a?(Array) }, + is_hash: ->(val, _arg, _data) { val.is_a?(Hash) }, + is_json: ->(val, _arg, _data) { ParamValidation.is_valid_json?(val) }, + in_range: lambda { |val, arg, _data| + begin + arg.cover?(val) + rescue StandardError + false + end + }, + is_a: ->(val, arg, _data) { arg.is_a?(Enumerable) ? arg.any? { |i| val.is_a?(i) } : val.is_a?(arg) }, + can_be_date: ->(val, _arg, _data) { val.is_a?(Date) || val.is_a?(DateTime) || Chronic.parse(val) }, + array_of_hashes: ->(_val, arg, data) { data.is_a?(Array) && data.map { |pair| ParamValidation.new(pair.to_h, arg) }.all? } } @@messages = { - required: lambda {|h| "#{h[:key]} is required"}, - absent: lambda {|h| "#{h[:key]} must not be present"}, - not_blank: lambda {|h| "#{h[:key]} must not be blank"}, - not_included_in: lambda {|h| "#{h[:key]} must not be included in #{h[:arg].join(", ")}"}, - included_in: lambda {|h|"#{h[:key]} must be one of #{h[:arg].join(", ")}"}, - format: lambda {|h|"#{h[:key]} doesn't have the right format"}, - is_integer: lambda {|h|"#{h[:key]} should be an integer"}, - is_float: lambda {|h|"#{h[:key]} should be a float"}, - min_length: lambda {|h|"#{h[:key]} has a minimum length of #{h[:arg]}"}, - max_length: lambda {|h|"#{h[:key]} has a maximum length of #{h[:arg]}"}, - length_range: lambda {|h|"#{h[:key]} should have a length within #{h[:arg]}"}, - length_equals: lambda {|h|"#{h[:key]} should have a length of #{h[:arg]}"}, - is_reference: lambda{|h| "#{h[:key]} should be an integer or blank"}, - equals: lambda {|h|"#{h[:key]} should equal #{h[:arg]}"}, - min: lambda {|h|"#{h[:key]} must be at least #{h[:arg]}"}, - max: lambda {|h|"#{h[:key]} cannot be more than #{h[:arg]}"}, - in_range: lambda {|h|"#{h[:key]} should be within #{h[:arg]}"}, - is_json: lambda {|h| "#{h[:key]} should be valid JSON"}, - is_hash: lambda {|h| "#{h[:key]} should be a hash"}, - is_a: lambda {|h| "#{h[:key]} should be of the type(s): #{h[:arg].kind_of?(Enumerable) ? h[:arg].join(', '): h[:arg]}"}, - can_be_date: lambda {|h| "#{h[:key]} should be a datetime or be parsable as one"}, - array_of_hashes: lambda {|h| "Please pass in an array of hashes"} + required: ->(h) { "#{h[:key]} is required" }, + absent: ->(h) { "#{h[:key]} must not be present" }, + not_blank: ->(h) { "#{h[:key]} must not be blank" }, + not_included_in: ->(h) { "#{h[:key]} must not be included in #{h[:arg].join(', ')}" }, + included_in: ->(h) { "#{h[:key]} must be one of #{h[:arg].join(', ')}" }, + format: ->(h) { "#{h[:key]} doesn't have the right format" }, + is_integer: ->(h) { "#{h[:key]} should be an integer" }, + is_float: ->(h) { "#{h[:key]} should be a float" }, + min_length: ->(h) { "#{h[:key]} has a minimum length of #{h[:arg]}" }, + max_length: ->(h) { "#{h[:key]} has a maximum length of #{h[:arg]}" }, + length_range: ->(h) { "#{h[:key]} should have a length within #{h[:arg]}" }, + length_equals: ->(h) { "#{h[:key]} should have a length of #{h[:arg]}" }, + is_reference: ->(h) { "#{h[:key]} should be an integer or blank" }, + equals: ->(h) { "#{h[:key]} should equal #{h[:arg]}" }, + min: ->(h) { "#{h[:key]} must be at least #{h[:arg]}" }, + max: ->(h) { "#{h[:key]} cannot be more than #{h[:arg]}" }, + in_range: ->(h) { "#{h[:key]} should be within #{h[:arg]}" }, + is_json: ->(h) { "#{h[:key]} should be valid JSON" }, + is_hash: ->(h) { "#{h[:key]} should be a hash" }, + is_a: ->(h) { "#{h[:key]} should be of the type(s): #{h[:arg].is_a?(Enumerable) ? h[:arg].join(', ') : h[:arg]}" }, + can_be_date: ->(h) { "#{h[:key]} should be a datetime or be parsable as one" }, + array_of_hashes: ->(_h) { 'Please pass in an array of hashes' } } # small utility for testing json validity def self.is_valid_json?(str) - begin - JSON.parse(str) - return true - rescue => e - return false - end + JSON.parse(str) + true + rescue StandardError => e + false end # Special error class that holds all the error data for reference @@ -125,6 +196,4 @@ class ParamValidation super(msg) end end - end - diff --git a/gems/ruby-param-validation/param_validation.gemspec b/gems/ruby-param-validation/param_validation.gemspec index 56d8931f..751a4c8f 100644 --- a/gems/ruby-param-validation/param_validation.gemspec +++ b/gems/ruby-param-validation/param_validation.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| s.name = 'param_validation' s.version = '0.0.2' diff --git a/gems/ruby-param-validation/test/param_validation_test.rb b/gems/ruby-param-validation/test/param_validation_test.rb index a58262ef..cafe8435 100644 --- a/gems/ruby-param-validation/test/param_validation_test.rb +++ b/gems/ruby-param-validation/test/param_validation_test.rb @@ -1,173 +1,193 @@ +# frozen_string_literal: true + require './lib/param_validation.rb' require 'minitest/autorun' class ParamValidationTest < Minitest::Test - - def setup - end + def setup; end def test_required - begin; ParamValidation.new({}, {x: {required: true}}) + begin; ParamValidation.new({}, x: { required: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + # If a key is not required, then don't run the tests on it def test_not_required_and_absent_then_tests_do_not_run - ParamValidation.new({}, {x: {max: 100}}) + ParamValidation.new({}, x: { max: 100 }) assert true end + def test_not_blank_fail - begin; ParamValidation.new({x: ''}, {x: {not_blank: true}}) + begin; ParamValidation.new({ x: '' }, x: { not_blank: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_not_blank_fail_nil - begin; ParamValidation.new({x: nil}, {x: {not_blank: true, required: true}}) + begin; ParamValidation.new({ x: nil }, x: { not_blank: true, required: true }) rescue ParamValidation::ValidationError => e; e; end - assert(e.data.one?{|i| i[:name] == :not_blank && i[:key] == :x}) - assert(e.data.one?{|i| i[:name] == :required && i[:key] == :x}) + assert(e.data.one? { |i| i[:name] == :not_blank && i[:key] == :x }) + assert(e.data.one? { |i| i[:name] == :required && i[:key] == :x }) end + def test_not_blank_succeed - ParamValidation.new({x: 'x'}, {x: {not_blank: true}}) + ParamValidation.new({ x: 'x' }, x: { not_blank: true }) assert true end + def test_require_no_err - begin; ParamValidation.new({x: 1}, {x: {required: true}}) + begin; ParamValidation.new({ x: 1 }, x: { required: true }) rescue ParamValidation::ValidationError => e; end assert e.nil? end + def test_absent - begin; ParamValidation.new({x: 1}, {x: {absent: true}}) + begin; ParamValidation.new({ x: 1 }, x: { absent: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_not_included_in - begin; ParamValidation.new({x: 1}, {x: {not_included_in: [1]}}) + begin; ParamValidation.new({ x: 1 }, x: { not_included_in: [1] }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_included_in - begin; ParamValidation.new({x: 1}, {x: {included_in: [2]}}) + begin; ParamValidation.new({ x: 1 }, x: { included_in: [2] }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_format - begin; ParamValidation.new({x: 'x'}, {x: {format: /y/}}) + begin; ParamValidation.new({ x: 'x' }, x: { format: /y/ }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end def test_is_reference_string - begin; ParamValidation.new({x: '-0'}, {x: {is_reference: true}}) + begin; ParamValidation.new({ x: '-0' }, x: { is_reference: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end def test_is_reference_negative_integer - begin; ParamValidation.new({x: -1}, {x: {is_reference: true}}) + begin; ParamValidation.new({ x: -1 }, x: { is_reference: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end def test_is_reference_passes - ParamValidation.new({x: '0'}, {x: {is_reference: true}}) - ParamValidation.new({x: 1}, {x: {is_reference: true}}) - ParamValidation.new({x: ''}, {x: {is_reference: true}}) - pass() + ParamValidation.new({ x: '0' }, x: { is_reference: true }) + ParamValidation.new({ x: 1 }, x: { is_reference: true }) + ParamValidation.new({ x: '' }, x: { is_reference: true }) + pass end def test_is_integer - begin; ParamValidation.new({x: 'x'}, {x: {is_integer: true}}) + begin; ParamValidation.new({ x: 'x' }, x: { is_integer: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_is_float - begin; ParamValidation.new({x: 'x'}, {x: {is_float: true}}) + begin; ParamValidation.new({ x: 'x' }, x: { is_float: true }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_min_length - begin; ParamValidation.new({x: []}, {x: {min_length: 2}}) + begin; ParamValidation.new({ x: [] }, x: { min_length: 2 }) rescue ParamValidation::ValidationError => e; e; end assert_equal :x, e.data[:key] end + def test_max_length - begin; ParamValidation.new({x: [1,2,3]}, {x: {max_length: 2}}) + begin; ParamValidation.new({ x: [1, 2, 3] }, x: { max_length: 2 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:key], :x end + def test_length_range - begin; ParamValidation.new({x: [1,2,3,4]}, {x: {length_range: 1..3}}) + begin; ParamValidation.new({ x: [1, 2, 3, 4] }, x: { length_range: 1..3 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:key], :x end + def test_length_equals - begin; ParamValidation.new({x: [1,2]}, {x: {length_equals: 1}}) + begin; ParamValidation.new({ x: [1, 2] }, x: { length_equals: 1 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:key], :x end + def test_min - begin; ParamValidation.new({x: 1}, {x: {min: 2}}) + begin; ParamValidation.new({ x: 1 }, x: { min: 2 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:key], :x end + def test_max - begin; ParamValidation.new({x: 4}, {x: {max: 2}}) + begin; ParamValidation.new({ x: 4 }, x: { max: 2 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:name], :max end + def test_in_range - begin; ParamValidation.new({x: 1}, {x: {in_range: 2..4}}) + begin; ParamValidation.new({ x: 1 }, x: { in_range: 2..4 }) rescue ParamValidation::ValidationError => e; e; end assert_equal e.data[:val], 1 end + def test_equals - begin; ParamValidation.new({x: 1}, {x: {equals: 2}}) + begin; ParamValidation.new({ x: 1 }, x: { equals: 2 }) rescue ParamValidation::ValidationError => e; e; end - assert_equal "x should equal #{2}", e.to_s + assert_equal 'x should equal 2', e.to_s end + def test_root_array_of_hashes - begin; ParamValidation.new({x: 1}, {root: {array_of_hashes: {x: {required: true}}}}) + begin; ParamValidation.new({ x: 1 }, root: { array_of_hashes: { x: { required: true } } }) rescue ParamValidation::ValidationError => e; e; end - assert_equal "Please pass in an array of hashes", e.to_s + assert_equal 'Please pass in an array of hashes', e.to_s end + def test_root_array_of_hashes_with_nesting_ok - v = ParamValidation.new([{'x' => 1}, {x: 1}], {root: {array_of_hashes: {x: {is_integer: true}}}}) + v = ParamValidation.new([{ 'x' => 1 }, { x: 1 }], root: { array_of_hashes: { x: { is_integer: true } } }) assert_equal v, v # test that it does not raise end + def test_root_array_of_hashes_with_nesting - begin; ParamValidation.new([{x: 1}, {x: 'hi'}], {root: {array_of_hashes: {x: {is_integer: true}}}}) + begin; ParamValidation.new([{ x: 1 }, { x: 'hi' }], root: { array_of_hashes: { x: { is_integer: true } } }) rescue ParamValidation::ValidationError => e; e; end - assert_equal "x should be an integer", e.to_s + assert_equal 'x should be an integer', e.to_s end def test_is_json_with_string - begin; ParamValidation.new({x: '[[[[[[['}, {x: {is_json: true}}) + begin; ParamValidation.new({ x: '[[[[[[[' }, x: { is_json: true }) rescue ParamValidation::ValidationError => e; e; end - assert_equal "x should be valid JSON", e.to_s + assert_equal 'x should be valid JSON', e.to_s end def test_is_json_without_string - begin; ParamValidation.new({x: {}}, {x: {is_json: true}}) + begin; ParamValidation.new({ x: {} }, x: { is_json: true }) rescue ParamValidation::ValidationError => e; e; end - assert_equal "x should be valid JSON", e.to_s + assert_equal 'x should be valid JSON', e.to_s end def test_is_a_single - ParamValidation.new({x: 5.6}, {x: {is_a: Float}}) + ParamValidation.new({ x: 5.6 }, x: { is_a: Float }) begin - ParamValidation.new({x: 5.6}, {x: {is_a: Integer}}) + ParamValidation.new({ x: 5.6 }, x: { is_a: Integer }) rescue ParamValidation::ValidationError => e e end assert_equal 'x should be of the type(s): Integer', e.to_s end - def test_is_a_multiple - ParamValidation.new({x: 5.6}, {x: {is_a: [Integer,Float]}}) + ParamValidation.new({ x: 5.6 }, x: { is_a: [Integer, Float] }) begin - ParamValidation.new({x: 5.6}, {x: {is_a: [Integer, Array]}}) + ParamValidation.new({ x: 5.6 }, x: { is_a: [Integer, Array] }) rescue ParamValidation::ValidationError => e e end @@ -176,13 +196,13 @@ class ParamValidationTest < Minitest::Test end def test_can_be_date - ParamValidation.new({x: Date.new()}, {x: {can_be_date: true}}) - ParamValidation.new({x: DateTime.new()}, {x: {can_be_date: true}}) - ParamValidation.new({x: '2017-05-15T12:00:00.000Z'}, {x: {can_be_date: true}}) - ParamValidation.new({x: '2017-05-15'}, {x: {can_be_date: true}}) + ParamValidation.new({ x: Date.new }, x: { can_be_date: true }) + ParamValidation.new({ x: DateTime.new }, x: { can_be_date: true }) + ParamValidation.new({ x: '2017-05-15T12:00:00.000Z' }, x: { can_be_date: true }) + ParamValidation.new({ x: '2017-05-15' }, x: { can_be_date: true }) begin - ParamValidation.new({x: 'not_a _date'}, {x: {can_be_date: true}}) + ParamValidation.new({ x: 'not_a _date' }, x: { can_be_date: true }) rescue ParamValidation::ValidationError => e e end @@ -191,25 +211,25 @@ class ParamValidationTest < Minitest::Test end def test_add_validator - ParamValidation.add_validator(:dollars){|val, arg, data| val =~ /^\d+(\.\d\d)?$/} + ParamValidation.add_validator(:dollars) { |val, _arg, _data| val =~ /^\d+(\.\d\d)?$/ } begin - ParamValidation.new({x: 'hi'}, {x: {dollars: true}}) + ParamValidation.new({ x: 'hi' }, x: { dollars: true }) rescue ParamValidation::ValidationError => e e end assert_equal :dollars, e.data[:name] end + def test_set_message - ParamValidation.add_validator(:dollars){|val, arg, data| val =~ /^\d+(\.\d\d)?$/} - ParamValidation.set_message(:dollars){|h| "#{h[:key]} must be a dollar amount"} + ParamValidation.add_validator(:dollars) { |val, _arg, _data| val =~ /^\d+(\.\d\d)?$/ } + ParamValidation.set_message(:dollars) { |h| "#{h[:key]} must be a dollar amount" } begin - ParamValidation.new({x: 'hi'}, {x: {dollars: true}}) + ParamValidation.new({ x: 'hi' }, x: { dollars: true }) rescue ParamValidation::ValidationError => e e end - assert_equal "x must be a dollar amount", e.to_s + assert_equal 'x must be a dollar amount', e.to_s end - def test_custom_validator - end + def test_custom_validator; end end diff --git a/gems/ruby-qx/lib/qx.rb b/gems/ruby-qx/lib/qx.rb index 5434c1e8..f5ee40e1 100644 --- a/gems/ruby-qx/lib/qx.rb +++ b/gems/ruby-qx/lib/qx.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require 'active_record' require 'colorize' @@ -39,9 +41,9 @@ class Qx str += expr[:JOIN].map { |from, cond| " JOIN #{from} ON #{cond}" }.join if expr[:JOIN] str += expr[:LEFT_JOIN].map { |from, cond| " LEFT JOIN #{from} ON #{cond}" }.join if expr[:LEFT_JOIN] str += expr[:LEFT_OUTER_JOIN].map { |from, cond| " LEFT OUTER JOIN #{from} ON #{cond}" }.join if expr[:LEFT_OUTER_JOIN] - str += expr[:JOIN_LATERAL].map {|i| " JOIN LATERAL (#{i[:select_statement]}) #{i[:join_name]} ON #{i[:success_condition]}"}.join if expr[:JOIN_LATERAL] + str += expr[:JOIN_LATERAL].map { |i| " JOIN LATERAL (#{i[:select_statement]}) #{i[:join_name]} ON #{i[:success_condition]}" }.join if expr[:JOIN_LATERAL] - str += expr[:LEFT_JOIN_LATERAL].map {|i| " LEFT JOIN LATERAL (#{i[:select_statement]}) #{i[:join_name]} ON #{i[:success_condition]}"}.join if expr[:LEFT_JOIN_LATERAL] + str += expr[:LEFT_JOIN_LATERAL].map { |i| " LEFT JOIN LATERAL (#{i[:select_statement]}) #{i[:join_name]} ON #{i[:success_condition]}" }.join if expr[:LEFT_JOIN_LATERAL] str += ' WHERE ' + expr[:WHERE].map { |w| "(#{w})" }.join(' AND ') if expr[:WHERE] str += ' GROUP BY ' + expr[:GROUP_BY].join(', ') if expr[:GROUP_BY] str += ' HAVING ' + expr[:HAVING].map { |h| "(#{h})" }.join(' AND ') if expr[:HAVING] @@ -77,9 +79,9 @@ class Qx elsif expr[:ON_CONSTRAINT] str += " ON CONSTRAINT #{expr[:ON_CONSTRAINT]}" end - str += ' DO NOTHING' if !expr[:CONFLICT_UPSERT] + str += ' DO NOTHING' unless expr[:CONFLICT_UPSERT] if expr[:CONFLICT_UPSERT] - set_str = expr[:INSERT_COLUMNS].select{|i| i != 'created_at'}.map{|i| "#{i} = EXCLUDED.#{i}" } + set_str = expr[:INSERT_COLUMNS].reject { |i| i == 'created_at' }.map { |i| "#{i} = EXCLUDED.#{i}" } str += " DO UPDATE SET #{set_str.join(', ')}" end end @@ -101,6 +103,7 @@ class Qx str += ' ' + expr[:ON_CONFLICT] if expr[:ON_CONFLICT] str += ' RETURNING ' + expr[:RETURNING].join(', ') if expr[:RETURNING] end + str end @@ -121,6 +124,7 @@ class Qx # Qx.execute(Qx.select("id").from("table_name")) def self.execute(expr, data = {}, options = {}) return expr.execute(data) if expr.is_a?(Qx) + interpolated = Qx.interpolate_expr(expr, data) execute_raw(interpolated, options) end @@ -247,7 +251,7 @@ class Qx def order_by(*cols) orders = /(asc)|(desc)( nulls (first)|(last))?/i # Sanitize out invalid order keywords - @tree[:ORDER_BY] = cols.map { |col, order| [col.to_s, order.to_s.downcase.strip.match(order.to_s.downcase) ? order.to_s.upcase : nil] } + @tree[:ORDER_BY] = cols.map { |col, order| [col.to_s, order.to_s.downcase.strip.match?(order.to_s.downcase) ? order.to_s.upcase : nil] } self end @@ -310,18 +314,15 @@ class Qx self end - def join_lateral(join_name, select_statement, success_condition=true) - + def join_lateral(join_name, select_statement, success_condition = true) @tree[:JOIN_LATERAL] ||= [] - @tree[:JOIN_LATERAL].concat([{join_name: join_name, select_statement: select_statement, success_condition: success_condition}]) + @tree[:JOIN_LATERAL].concat([{ join_name: join_name, select_statement: select_statement, success_condition: success_condition }]) self end - - def left_join_lateral(join_name, select_statement, success_condition=true) - + def left_join_lateral(join_name, select_statement, success_condition = true) @tree[:LEFT_JOIN_LATERAL] ||= [] - @tree[:LEFT_JOIN_LATERAL].concat([{join_name: join_name, select_statement: select_statement, success_condition: success_condition}]) + @tree[:LEFT_JOIN_LATERAL].concat([{ join_name: join_name, select_statement: select_statement, success_condition: success_condition }]) self end @@ -384,7 +385,7 @@ class Qx self end - def on_conflict() + def on_conflict @tree[:ON_CONFLICT] = true self end @@ -399,8 +400,8 @@ class Qx self end - def upsert(on_index, columns=nil) - @tree[:CONFLICT_UPSERT] = {index: on_index, cols: columns} + def upsert(on_index, columns = nil) + @tree[:CONFLICT_UPSERT] = { index: on_index, cols: columns } self end diff --git a/gems/ruby-qx/qx.gemspec b/gems/ruby-qx/qx.gemspec index dcfcd007..195b126b 100644 --- a/gems/ruby-qx/qx.gemspec +++ b/gems/ruby-qx/qx.gemspec @@ -1,3 +1,5 @@ +# frozen_string_literal: true + Gem::Specification.new do |s| s.name = 'qx' s.version = '0.1.1' @@ -9,7 +11,7 @@ Gem::Specification.new do |s| s.files = 'lib/qx.rb' s.homepage = 'https://github.com/jayrbolton/qx' s.license = 'MIT' - s.add_runtime_dependency 'colorize', '~> 0.8' s.add_runtime_dependency 'activerecord', '>= 3.0' + s.add_runtime_dependency 'colorize', '~> 0.8' s.add_development_dependency 'minitest', '~> 5.9' end diff --git a/gems/ruby-qx/test/UpsertTest.rb b/gems/ruby-qx/test/UpsertTest.rb index 1638669f..909d2fb7 100644 --- a/gems/ruby-qx/test/UpsertTest.rb +++ b/gems/ruby-qx/test/UpsertTest.rb @@ -1,20 +1,20 @@ +# frozen_string_literal: true + require './lib/qx.rb' require 'minitest/autorun' class UpsertTest < Minitest::Test - def setup - - end + def setup; end def test_upsert table = 'x' - column1 = "a" + column1 = 'a' column2 = 'b' - idx = "idx_something_more" + idx = 'idx_something_more' - result = Qx.insert_into(table).values({column1: column1, column2: column2}).on_conflict.upsert(idx).parse + result = Qx.insert_into(table).values(column1: column1, column2: column2).on_conflict.upsert(idx).parse - expected = %Q(INSERT INTO "#{table}" ("column1", "column2") VALUES ($Q$#{column1}$Q$, $Q$#{column2}$Q$) ON CONFLICT ON CONSTRAINT #{idx} DO UPDATE SET "column1" = EXCLUDED."column1", "column2" = EXCLUDED."column2") + expected = %(INSERT INTO "#{table}" ("column1", "column2") VALUES ($Q$#{column1}$Q$, $Q$#{column2}$Q$) ON CONFLICT ON CONSTRAINT #{idx} DO UPDATE SET "column1" = EXCLUDED."column1", "column2" = EXCLUDED."column2") assert_equal(expected, result) end -end \ No newline at end of file +end diff --git a/gems/ruby-qx/test/qx_test.rb b/gems/ruby-qx/test/qx_test.rb index 74fd2ee8..71c497ae 100644 --- a/gems/ruby-qx/test/qx_test.rb +++ b/gems/ruby-qx/test/qx_test.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + require './lib/qx.rb' require 'pg' require 'minitest/autorun' @@ -10,9 +12,7 @@ Qx.config(type_map: tm) Qx.execute_file('./test/test_schema.sql') class QxTest < Minitest::Test - - def setup - end + def setup; end # Let's just test that the schema was executed def test_execute_file @@ -23,241 +23,260 @@ class QxTest < Minitest::Test end def test_select_from - parsed = Qx.select(:id, "name").from(:table_name).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name) + parsed = Qx.select(:id, 'name').from(:table_name).parse + assert_equal parsed, %(SELECT id, name FROM table_name) end + def test_select_distinct_on - parsed = Qx.select(:id, "name").distinct_on(:distinct_col1, :distinct_col2).from(:table_name).parse - assert_equal parsed, %Q(SELECT DISTINCT ON (distinct_col1, distinct_col2) id, name FROM table_name) + parsed = Qx.select(:id, 'name').distinct_on(:distinct_col1, :distinct_col2).from(:table_name).parse + assert_equal parsed, %(SELECT DISTINCT ON (distinct_col1, distinct_col2) id, name FROM table_name) end + def test_select_distinct - parsed = Qx.select(:id, "name").distinct.from(:table_name).parse - assert_equal parsed, %Q(SELECT DISTINCT id, name FROM table_name) + parsed = Qx.select(:id, 'name').distinct.from(:table_name).parse + assert_equal parsed, %(SELECT DISTINCT id, name FROM table_name) end def test_select_as - parsed = Qx.select(:id, "name").from(:table_name).as(:alias).parse - assert_equal parsed, %Q((SELECT id, name FROM table_name) AS "alias") + parsed = Qx.select(:id, 'name').from(:table_name).as(:alias).parse + assert_equal parsed, %((SELECT id, name FROM table_name) AS "alias") end - + def test_select_where - parsed = Qx.select(:id, "name").from(:table_name).where("x = $y OR a = $b", y: 1, b: 2).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name WHERE (x = 1 OR a = 2)) + parsed = Qx.select(:id, 'name').from(:table_name).where('x = $y OR a = $b', y: 1, b: 2).parse + assert_equal parsed, %(SELECT id, name FROM table_name WHERE (x = 1 OR a = 2)) end + def test_select_where_hash_array - parsed = Qx.select(:id, "name").from(:table_name).where([x: 1], ["y = $n", {n: 2}]).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name WHERE ("x" IN (1)) AND (y = 2)) + parsed = Qx.select(:id, 'name').from(:table_name).where([x: 1], ['y = $n', { n: 2 }]).parse + assert_equal parsed, %(SELECT id, name FROM table_name WHERE ("x" IN (1)) AND (y = 2)) end + def test_select_and_where - parsed = Qx.select(:id, "name").from(:table_name).where("x = $y", y: 1).and_where("a = $b", b: 2).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name WHERE (x = 1) AND (a = 2)) + parsed = Qx.select(:id, 'name').from(:table_name).where('x = $y', y: 1).and_where('a = $b', b: 2).parse + assert_equal parsed, %(SELECT id, name FROM table_name WHERE (x = 1) AND (a = 2)) end + def test_select_and_where_hash - parsed = Qx.select(:id, "name").from(:table_name).where("x = $y", y: 1).and_where(a: 2).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name WHERE (x = 1) AND ("a" IN (2))) + parsed = Qx.select(:id, 'name').from(:table_name).where('x = $y', y: 1).and_where(a: 2).parse + assert_equal parsed, %(SELECT id, name FROM table_name WHERE (x = 1) AND ("a" IN (2))) end - + def test_select_and_group_by - parsed = Qx.select(:id, "name").from(:table_name).group_by("col1", "col2").parse - assert_equal parsed, %Q(SELECT id, name FROM table_name GROUP BY col1, col2) + parsed = Qx.select(:id, 'name').from(:table_name).group_by('col1', 'col2').parse + assert_equal parsed, %(SELECT id, name FROM table_name GROUP BY col1, col2) end - + def test_select_and_order_by - parsed = Qx.select(:id, "name").from(:table_name).order_by("col1", ["col2", "DESC NULLS LAST"]).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name ORDER BY col1 , col2 DESC NULLS LAST) + parsed = Qx.select(:id, 'name').from(:table_name).order_by('col1', ['col2', 'DESC NULLS LAST']).parse + assert_equal parsed, %(SELECT id, name FROM table_name ORDER BY col1 , col2 DESC NULLS LAST) end def test_select_having - parsed = Qx.select(:id, "name").from(:table_name).having("COUNT(col1) > $n", n: 1).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name HAVING (COUNT(col1) > 1)) + parsed = Qx.select(:id, 'name').from(:table_name).having('COUNT(col1) > $n', n: 1).parse + assert_equal parsed, %(SELECT id, name FROM table_name HAVING (COUNT(col1) > 1)) end + def test_select_and_having - parsed = Qx.select(:id, "name").from(:table_name).having("COUNT(col1) > $n", n: 1).and_having("SUM(col2) > $m", m: 2).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name HAVING (COUNT(col1) > 1) AND (SUM(col2) > 2)) + parsed = Qx.select(:id, 'name').from(:table_name).having('COUNT(col1) > $n', n: 1).and_having('SUM(col2) > $m', m: 2).parse + assert_equal parsed, %(SELECT id, name FROM table_name HAVING (COUNT(col1) > 1) AND (SUM(col2) > 2)) end def test_select_limit - parsed = Qx.select(:id, "name").from(:table_name).limit(10).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name LIMIT 10) + parsed = Qx.select(:id, 'name').from(:table_name).limit(10).parse + assert_equal parsed, %(SELECT id, name FROM table_name LIMIT 10) end + def test_select_offset - parsed = Qx.select(:id, "name").from(:table_name).offset(10).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name OFFSET 10) + parsed = Qx.select(:id, 'name').from(:table_name).offset(10).parse + assert_equal parsed, %(SELECT id, name FROM table_name OFFSET 10) end def test_select_join - parsed = Qx.select(:id, "name").from(:table_name).join(['assoc1', 'assoc1.table_name_id=table_name.id']).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name JOIN assoc1 ON assoc1.table_name_id=table_name.id) + parsed = Qx.select(:id, 'name').from(:table_name).join(['assoc1', 'assoc1.table_name_id=table_name.id']).parse + assert_equal parsed, %(SELECT id, name FROM table_name JOIN assoc1 ON assoc1.table_name_id=table_name.id) end + def test_select_add_join - parsed = Qx.select(:id, "name").from(:table_name).join('assoc1', 'assoc1.table_name_id=table_name.id') - .add_join(['assoc2', 'assoc2.table_name_id=table_name.id']).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name JOIN assoc1 ON assoc1.table_name_id=table_name.id JOIN assoc2 ON assoc2.table_name_id=table_name.id) + parsed = Qx.select(:id, 'name').from(:table_name).join('assoc1', 'assoc1.table_name_id=table_name.id') + .add_join(['assoc2', 'assoc2.table_name_id=table_name.id']).parse + assert_equal parsed, %(SELECT id, name FROM table_name JOIN assoc1 ON assoc1.table_name_id=table_name.id JOIN assoc2 ON assoc2.table_name_id=table_name.id) end + def test_select_left_join - parsed = Qx.select(:id, "name").from(:table_name).left_join(['assoc1', 'assoc1.table_name_id=table_name.id']).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name LEFT JOIN assoc1 ON assoc1.table_name_id=table_name.id) + parsed = Qx.select(:id, 'name').from(:table_name).left_join(['assoc1', 'assoc1.table_name_id=table_name.id']).parse + assert_equal parsed, %(SELECT id, name FROM table_name LEFT JOIN assoc1 ON assoc1.table_name_id=table_name.id) end + def test_select_add_left_join - parsed = Qx.select(:id, "name").from(:table_name).left_join('assoc1', 'assoc1.table_name_id=table_name.id') - .add_left_join(['assoc2', 'assoc2.table_name_id=table_name.id']).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name LEFT JOIN assoc1 ON assoc1.table_name_id=table_name.id LEFT JOIN assoc2 ON assoc2.table_name_id=table_name.id) + parsed = Qx.select(:id, 'name').from(:table_name).left_join('assoc1', 'assoc1.table_name_id=table_name.id') + .add_left_join(['assoc2', 'assoc2.table_name_id=table_name.id']).parse + assert_equal parsed, %(SELECT id, name FROM table_name LEFT JOIN assoc1 ON assoc1.table_name_id=table_name.id LEFT JOIN assoc2 ON assoc2.table_name_id=table_name.id) end def test_select_where_subquery - parsed = Qx.select(:id, "name").from(:table_name).where("id IN ($ids)", ids: Qx.select("id").from("assoc")).parse - assert_equal parsed, %Q(SELECT id, name FROM table_name WHERE (id IN (SELECT id FROM assoc))) + parsed = Qx.select(:id, 'name').from(:table_name).where('id IN ($ids)', ids: Qx.select('id').from('assoc')).parse + assert_equal parsed, %(SELECT id, name FROM table_name WHERE (id IN (SELECT id FROM assoc))) end def test_select_join_subquery - parsed = Qx.select(:id).from(:table).join([Qx.select(:id).from(:assoc).as(:assoc), "assoc.table_id=table.id"]).parse - assert_equal parsed, %Q(SELECT id FROM table JOIN (SELECT id FROM assoc) AS "assoc" ON assoc.table_id=table.id) + parsed = Qx.select(:id).from(:table).join([Qx.select(:id).from(:assoc).as(:assoc), 'assoc.table_id=table.id']).parse + assert_equal parsed, %(SELECT id FROM table JOIN (SELECT id FROM assoc) AS "assoc" ON assoc.table_id=table.id) end def test_select_from_subquery parsed = Qx.select(:id).from(Qx.select(:id).from(:table).as(:table)).parse - assert_equal parsed, %Q(SELECT id FROM (SELECT id FROM table) AS "table") + assert_equal parsed, %(SELECT id FROM (SELECT id FROM table) AS "table") end def test_select_integration parsed = Qx.select(:id) - .from(:table) - .join([Qx.select(:id).from(:assoc).as(:assoc), 'assoc.table_id=table.id']) - .left_join(['lefty', 'lefty.table_id=table.id']) - .where('x = $n', n: 1) - .and_where('y = $n', n: 1) - .group_by(:x) - .order_by(:y) - .having('COUNT(x) > $n', n: 1) - .and_having('COUNT(y) > $n', n: 1) - .limit(10) - .offset(10) - .parse - assert_equal parsed, %Q(SELECT id FROM table JOIN (SELECT id FROM assoc) AS "assoc" ON assoc.table_id=table.id LEFT JOIN lefty ON lefty.table_id=table.id WHERE (x = 1) AND (y = 1) GROUP BY x HAVING (COUNT(x) > 1) AND (COUNT(y) > 1) ORDER BY y LIMIT 10 OFFSET 10) + .from(:table) + .join([Qx.select(:id).from(:assoc).as(:assoc), 'assoc.table_id=table.id']) + .left_join(['lefty', 'lefty.table_id=table.id']) + .where('x = $n', n: 1) + .and_where('y = $n', n: 1) + .group_by(:x) + .order_by(:y) + .having('COUNT(x) > $n', n: 1) + .and_having('COUNT(y) > $n', n: 1) + .limit(10) + .offset(10) + .parse + assert_equal parsed, %(SELECT id FROM table JOIN (SELECT id FROM assoc) AS "assoc" ON assoc.table_id=table.id LEFT JOIN lefty ON lefty.table_id=table.id WHERE (x = 1) AND (y = 1) GROUP BY x HAVING (COUNT(x) > 1) AND (COUNT(y) > 1) ORDER BY y LIMIT 10 OFFSET 10) end def test_insert_into_values_hash parsed = Qx.insert_into(:table_name).values(x: 1).parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x") VALUES (1)) + assert_equal parsed, %(INSERT INTO "table_name" ("x") VALUES (1)) end + def test_insert_into_values_hash_array - parsed = Qx.insert_into(:table_name).values([{x: 1}, {x: 2}]).parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x") VALUES (1), (2)) + parsed = Qx.insert_into(:table_name).values([{ x: 1 }, { x: 2 }]).parse + assert_equal parsed, %(INSERT INTO "table_name" ("x") VALUES (1), (2)) end + def test_insert_into_values_csv_style parsed = Qx.insert_into(:table_name).values([['x'], [1], [2]]).parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x") VALUES (1), (2)) + assert_equal parsed, %(INSERT INTO "table_name" ("x") VALUES (1), (2)) end + def test_insert_into_values_common_values - parsed = Qx.insert_into(:table_name).values([{x: 'bye'}, {x: 'hi'}]).common_values(z: 1).parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x", "z") VALUES ($Q$bye$Q$, 1), ($Q$hi$Q$, 1)) + parsed = Qx.insert_into(:table_name).values([{ x: 'bye' }, { x: 'hi' }]).common_values(z: 1).parse + assert_equal parsed, %(INSERT INTO "table_name" ("x", "z") VALUES ($Q$bye$Q$, 1), ($Q$hi$Q$, 1)) end + def test_insert_into_values_timestamps parsed = Qx.insert_into(:table_name).values(x: 1).ts.parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x", created_at, updated_at) VALUES (1, '#{Time.now.utc}', '#{Time.now.utc}')) + assert_equal parsed, %(INSERT INTO "table_name" ("x", created_at, updated_at) VALUES (1, '#{Time.now.utc}', '#{Time.now.utc}')) end + def test_insert_into_values_returning parsed = Qx.insert_into(:table_name).values(x: 1).returning('*').parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x") VALUES (1) RETURNING *) + assert_equal parsed, %(INSERT INTO "table_name" ("x") VALUES (1) RETURNING *) end + def test_insert_into_select - parsed = Qx.insert_into(:table_name, ['hi']).select('hi').from(:table2).where("x=y").parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("hi") SELECT hi FROM table2 WHERE (x=y)) + parsed = Qx.insert_into(:table_name, ['hi']).select('hi').from(:table2).where('x=y').parse + assert_equal parsed, %(INSERT INTO "table_name" ("hi") SELECT hi FROM table2 WHERE (x=y)) end def test_update_set - parsed = Qx.update(:table_name).set(x: 1).where("y = 2").parse - assert_equal parsed, %Q(UPDATE "table_name" SET "x" = 1 WHERE (y = 2)) + parsed = Qx.update(:table_name).set(x: 1).where('y = 2').parse + assert_equal parsed, %(UPDATE "table_name" SET "x" = 1 WHERE (y = 2)) end + def test_update_timestamps now = Time.now.utc - parsed = Qx.update(:table_name).set(x: 1).where("y = 2").timestamps.parse - assert_equal parsed, %Q(UPDATE "table_name" SET "x" = 1, updated_at = '#{now}' WHERE (y = 2)) + parsed = Qx.update(:table_name).set(x: 1).where('y = 2').timestamps.parse + assert_equal parsed, %(UPDATE "table_name" SET "x" = 1, updated_at = '#{now}' WHERE (y = 2)) end def test_update_on_conflict - Qx.update(:table_name).set(x: 1).where("y = 2").on_conflict(:nothing).parse - assert_equal parsed, %Q(UPDATE "table_name" SET "x" = 1 WHERE (y = 2) ON CONFLICT DO NOTHING) + Qx.update(:table_name).set(x: 1).where('y = 2').on_conflict(:nothing).parse + assert_equal parsed, %(UPDATE "table_name" SET "x" = 1 WHERE (y = 2) ON CONFLICT DO NOTHING) end def test_insert_timestamps now = Time.now.utc - parsed = Qx.insert_into(:table_name).values({x: 1}).ts.parse - assert_equal parsed, %Q(INSERT INTO "table_name" ("x", created_at, updated_at) VALUES (1, '#{now}', '#{now}')) + parsed = Qx.insert_into(:table_name).values(x: 1).ts.parse + assert_equal parsed, %(INSERT INTO "table_name" ("x", created_at, updated_at) VALUES (1, '#{now}', '#{now}')) end def test_delete_from parsed = Qx.delete_from(:table_name).where(x: 1).parse - assert_equal parsed, %Q(DELETE FROM "table_name" WHERE ("x" IN (1))) + assert_equal parsed, %(DELETE FROM "table_name" WHERE ("x" IN (1))) end def test_pagination parsed = Qx.select(:x).from(:y).paginate(4, 30).parse - assert_equal parsed, %Q(SELECT x FROM y LIMIT 30 OFFSET 90) + assert_equal parsed, %(SELECT x FROM y LIMIT 30 OFFSET 90) end def test_execute_string - result = Qx.execute("SELECT * FROM (VALUES ($x)) AS t", x: 'x') - assert_equal result, [{'column1' => 'x'}] + result = Qx.execute('SELECT * FROM (VALUES ($x)) AS t', x: 'x') + assert_equal result, [{ 'column1' => 'x' }] end + def test_execute_format_csv - result = Qx.execute("SELECT * FROM (VALUES ($x)) AS t", {x: 'x'}, {format: 'csv'}) + result = Qx.execute('SELECT * FROM (VALUES ($x)) AS t', { x: 'x' }, format: 'csv') assert_equal result, [['column1'], ['x']] end + def test_execute_on_instances result = Qx.insert_into(:users).values(id: 1, email: 'uzr@example.com').execute - result = Qx.execute(Qx.select("*").from(:users).limit(1)) - assert_equal result, [{'id' => 1, 'email' => 'uzr@example.com'}] + result = Qx.execute(Qx.select('*').from(:users).limit(1)) + assert_equal result, [{ 'id' => 1, 'email' => 'uzr@example.com' }] Qx.delete_from(:users).where(id: 1).execute end def test_explain - parsed = Qx.select("*").from("table_name").explain.parse - assert_equal parsed, %Q(EXPLAIN SELECT * FROM table_name) + parsed = Qx.select('*').from('table_name').explain.parse + assert_equal parsed, %(EXPLAIN SELECT * FROM table_name) end # Manually test this one for now def test_pp_select - pp = Qx.select("id, name").from("table_name").where(status: 'active').and_where(id: Qx.select("id").from("roles").where(name: "admin")).pp + pp = Qx.select('id, name').from('table_name').where(status: 'active').and_where(id: Qx.select('id').from('roles').where(name: 'admin')).pp pp2 = Qx.insert_into(:table_name).values([x: 1, y: 2]).pp pp3 = Qx.update(:table_name).set(x: 1, y: 2).where(z: 1, a: 22).pp pp_delete = Qx.delete_from(:table_name).where(id: 123).pp - puts "" - puts "--- pretty print" + puts '' + puts '--- pretty print' puts pp puts pp2 puts pp3 puts pp_delete - puts "---" + puts '---' end def test_to_json parsed = Qx.select(:id).from(:users).to_json(:t).parse - assert_equal parsed, %Q(SELECT array_to_json(array_agg(row_to_json(t))) FROM (SELECT id FROM users) AS "t") + assert_equal parsed, %(SELECT array_to_json(array_agg(row_to_json(t))) FROM (SELECT id FROM users) AS "t") end def test_to_json_nested definitions = Qx.select(:part_of_speech, :body) - .from(:definitions) - .where("word_id=words.id") - .order_by("position ASC") - .to_json(:ds) - .as("definitions") + .from(:definitions) + .where('word_id=words.id') + .order_by('position ASC') + .to_json(:ds) + .as('definitions') parsed = Qx.select(:text, :pronunciation, definitions) - .from(:words) - .where("text='autumn'") - .to_json(:ws) - .parse + .from(:words) + .where("text='autumn'") + .to_json(:ws) + .parse assert_equal parsed, "SELECT array_to_json(array_agg(row_to_json(ws))) FROM (SELECT text, pronunciation, (SELECT array_to_json(array_agg(row_to_json(ds))) FROM (SELECT part_of_speech, body FROM definitions WHERE (word_id=words.id) ORDER BY position ASC ) AS \"ds\") AS \"definitions\" FROM words WHERE (text='autumn')) AS \"ws\"" end def test_copy_csv_execution - data = {'id' => '1', 'email' => 'uzr@example.com'} + data = { 'id' => '1', 'email' => 'uzr@example.com' } filename = '/tmp/qx-test.csv' Qx.insert_into(:users).values(data).ex - copy = Qx.select("*").from("users").execute(copy_csv: filename) + copy = Qx.select('*').from('users').execute(copy_csv: filename) contents = File.open(filename, 'r').read - csv_data = contents.split("\n").map{|l| l.split(",")} + csv_data = contents.split("\n").map { |l| l.split(',') } headers = csv_data.first row = csv_data.last assert_equal data.keys, headers @@ -265,11 +284,8 @@ class QxTest < Minitest::Test end def test_remove_clause - expr = Qx.select("*").from("table").limit(1) + expr = Qx.select('*').from('table').limit(1) expr = expr.remove_clause('limit') - assert_equal "SELECT * FROM table", expr.parse + assert_equal 'SELECT * FROM table', expr.parse end - - - end diff --git a/lib/audit.rb b/lib/audit.rb index a7ea242e..c2cd87a8 100644 --- a/lib/audit.rb +++ b/lib/audit.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Audit - # Given a list of pairs of nonprofit ids and stripe_account_ids (eg [[4341, 'acct_arst'], [3624, 'acct_arst']]) # Find all their available balances on both stripe and CC # Give all the ones that dont match up with the difference @@ -23,9 +24,9 @@ module Audit fees = p.payments.sum(:fee_total) net = p.payments.sum(:net_amount) puts [ - [p.gross_amount, p.fee_total, p.net_amount].join(", ") + " -- payout columns", - [gross, fees, net].join(", ") + " -- summed from payments", - [p.gross_amount - gross, p.fee_total - fees, p.net_amount - net].join(", ") + " -- differences" + [p.gross_amount, p.fee_total, p.net_amount].join(', ') + ' -- payout columns', + [gross, fees, net].join(', ') + ' -- summed from payments', + [p.gross_amount - gross, p.fee_total - fees, p.net_amount - net].join(', ') + ' -- differences' ].join("\n") end @@ -36,20 +37,21 @@ module Audit starting_after = nil transfers = [] loop do - new_transfers = Stripe::Transfer.all({limit: 100, starting_after: starting_after, destination: acct}).data + new_transfers = Stripe::Transfer.all(limit: 100, starting_after: starting_after, destination: acct).data break if new_transfers.empty? + transfers += new_transfers starting_after = new_transfers.last.id end ActiveRecord::Base.logger = logger - return transfers + transfers end # Given a list of Stripe transaction objects, see if any are missing on CommitChange def self.find_missing_charges(transfers) transfers - .map{|t| [t.source_transaction, t.amount]} - .select{|id, amount| Charge.where(stripe_charge_id: id, amount: amount).empty?} + .map { |t| [t.source_transaction, t.amount] } + .select { |id, amount| Charge.where(stripe_charge_id: id, amount: amount).empty? } end # Audit some basic balances for a nonprofit with those on Stripe @@ -58,7 +60,7 @@ module Audit puts "Stripe Dashboard: https://dashboard.stripe.com/#{np.stripe_account_id}" puts "CC Payments: https://commitchange.com/nonprofits/#{id}/payments" puts "CC Payouts: https://commitchange.com/nonprofits/#{id}/payouts" - + begin stripe_balances = Stripe::Balance.retrieve(stripe_account: np.stripe_account_id) available = stripe_balances['available'].first['amount'] @@ -74,36 +76,35 @@ module Audit cc_net: bal['net_amount'], diff: bal['net_amount'] - (available + pending) } - return data + data end - # Get the total gross, net + # Get the total gross, net # Pretty much duped from QueryPayments def self.np_balances(np_id) payment_ids_expr = Qx.select('DISTINCT payments.id') - .from(:payments) - .left_join( - [:charges, 'charges.payment_id=payments.id'], - [:refunds, 'refunds.payment_id=payments.id'], - [:disputes, 'disputes.payment_id=payments.id'] - ) - .where('payments.nonprofit_id=$id', id: np_id) - .and_where("refunds.payment_id IS NOT NULL OR charges.payment_id IS NOT NULL OR disputes.payment_id IS NOT NULL") - .and_where(%Q( + .from(:payments) + .left_join( + [:charges, 'charges.payment_id=payments.id'], + [:refunds, 'refunds.payment_id=payments.id'], + [:disputes, 'disputes.payment_id=payments.id'] + ) + .where('payments.nonprofit_id=$id', id: np_id) + .and_where('refunds.payment_id IS NOT NULL OR charges.payment_id IS NOT NULL OR disputes.payment_id IS NOT NULL') + .and_where(%( (refunds.payment_id IS NOT NULL AND (refunds.disbursed IS NULL OR refunds.disbursed='f')) OR (charges.status='available' OR charges.status='pending') OR (disputes.status='lost') )) - return Qx.select( - 'coalesce(SUM(payments.gross_amount), 0) AS gross_amount', - 'coalesce(SUM(payments.fee_total), 0) AS fee_total', - 'coalesce(SUM(payments.net_amount), 0) AS net_amount', - 'COUNT(payments.*) AS count' - ) + Qx.select( + 'coalesce(SUM(payments.gross_amount), 0) AS gross_amount', + 'coalesce(SUM(payments.fee_total), 0) AS fee_total', + 'coalesce(SUM(payments.net_amount), 0) AS net_amount', + 'COUNT(payments.*) AS count' + ) .from(:payments) - .where("payments.id IN ($ids)", ids: payment_ids_expr) + .where('payments.id IN ($ids)', ids: payment_ids_expr) .execute .first end - end diff --git a/lib/calculate/calculate_fees.rb b/lib/calculate/calculate_fees.rb index dc67ce49..846d2baf 100644 --- a/lib/calculate/calculate_fees.rb +++ b/lib/calculate/calculate_fees.rb @@ -1,16 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CalculateFees BaseFeeRate = 0.022 # 2.2% PerTransaction = 30 # 30 cents - def self.for_single_amount(amount, platform_fee=0.0) - ParamValidation.new({fee: platform_fee, amount: amount}, { - amount: {min: 0, is_integer: true}, - fee: {min: 0.0, is_float: true} - }) + def self.for_single_amount(amount, platform_fee = 0.0) + ParamValidation.new({ fee: platform_fee, amount: amount }, + amount: { min: 0, is_integer: true }, + fee: { min: 0.0, is_float: true }) fee = BaseFeeRate + platform_fee (amount * fee).ceil.to_i + PerTransaction end - end - diff --git a/lib/calculate/calculate_suggested_amounts.rb b/lib/calculate/calculate_suggested_amounts.rb index d4af6fab..4c8eaec8 100644 --- a/lib/calculate/calculate_suggested_amounts.rb +++ b/lib/calculate/calculate_suggested_amounts.rb @@ -1,50 +1,48 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'numeric' module CalculateSuggestedAmounts MIN = 25 - MAX = 100000000 - BRACKETS = [{range: MIN...1000, delta:100}, - {range: 1000...5000, delta: 500}, - {range: 5000...MAX, delta: 2500}] + MAX = 100_000_000 + BRACKETS = [{ range: MIN...1000, delta: 100 }, + { range: 1000...5000, delta: 500 }, + { range: 5000...MAX, delta: 2500 }].freeze # Calculates a set of suggested donation amounts based upon our internal special algorithm # This is most useful for suggesting amounts a recurring donor could change to # @return [Array] suggested amounts for your donation # @param [Number] amount the amount in cents to start from def self.calculate(amount) - ParamValidation.new({amount: amount}, amount: {required:true, is_a: Numeric, min: MIN, max:MAX }) + ParamValidation.new({ amount: amount }, amount: { required: true, is_a: Numeric, min: MIN, max: MAX }) result = [] step_down_val = step_down_value(amount) - unless step_down_val.nil? - result.push(step_down_val) - end + result.push(step_down_val) unless step_down_val.nil? higher_amounts = [] - while (higher_amounts.empty? || (higher_amounts.length < 3 && higher_amounts.last() != nil)) + while higher_amounts.empty? || (higher_amounts.length < 3 && !higher_amounts.last.nil?) if higher_amounts.empty? higher_amounts.push(step_up_value(amount)) else higher_amounts.push(step_up_value(higher_amounts.last)) end end - result.concat(higher_amounts.reject {|i| i.nil?}) + result.concat(higher_amounts.reject(&:nil?)) end def self.step_down_value(amount) - initial_bracket = get_bracket_by_amount(amount) - #check_floor_for_delta + # check_floor_for_delta delta_floor = amount.floor_for_delta(initial_bracket[:delta]) - #not on a delta, just send a floor - if (delta_floor != amount) + # not on a delta, just send a floor + if delta_floor != amount return delta_floor < MIN ? nil : delta_floor end - potential_lower_amount = amount - initial_bracket[:delta] # is potential_lower_amount < our MIN? if so, return nil @@ -52,30 +50,25 @@ module CalculateSuggestedAmounts new_bracket = get_bracket_by_amount(potential_lower_amount) - #if in same bracket, potential_lower_amount is our step_down_value + # if in same bracket, potential_lower_amount is our step_down_value - if initial_bracket == new_bracket - return potential_lower_amount - end + return potential_lower_amount if initial_bracket == new_bracket - #we're going to step down by our new bracket value then - return amount - new_bracket[:delta] + # we're going to step down by our new bracket value then + amount - new_bracket[:delta] end - def self.step_up_value(amount) - bracket = get_bracket_by_amount(amount) - #check_ceil_for_delta + # check_ceil_for_delta delta_ceil = amount.ceil_for_delta(bracket[:delta]) - #not on a delta, just send a ceil - if (delta_ceil != amount) + # not on a delta, just send a ceil + if delta_ceil != amount return delta_ceil >= MAX ? nil : delta_ceil end - potential_higher_amount = amount + bracket[:delta] # is potential_lower_amount < our MIN? if so, return nil @@ -84,10 +77,7 @@ module CalculateSuggestedAmounts potential_higher_amount end - - - def self.get_bracket_by_amount(amount) BRACKETS.select { |i| i[:range].cover?(amount) }.first end -end \ No newline at end of file +end diff --git a/lib/cancel_billing_subscription.rb b/lib/cancel_billing_subscription.rb index 75ea72c0..ef33f55c 100644 --- a/lib/cancel_billing_subscription.rb +++ b/lib/cancel_billing_subscription.rb @@ -1,19 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CancelBillingSubscription - # @param [Nonprofit] nonprofit # @return [BillingSubscription] new billing subscription for the nonprofit - def self.with_stripe(nonprofit) + def self.with_stripe(nonprofit) begin - ParamValidation.new({nonprofit: nonprofit}, { - nonprofit: {required: true, is_a: Nonprofit} - }) + ParamValidation.new({ nonprofit: nonprofit }, + nonprofit: { required: true, is_a: Nonprofit }) rescue ParamValidation::ValidationError => e - return {json: {error: "Validation error\n #{e.message}", errors: e.data}, status: :unprocessable_entity} + return { json: { error: "Validation error\n #{e.message}", errors: e.data }, status: :unprocessable_entity } end - np_card = nonprofit.active_card - billing_subscription = nonprofit.billing_subscription - return {json:{error: 'We don\'t have a subscription for your non-profit. Please contact support.'}, status: :unprocessable_entity} if np_card.nil? || billing_subscription.nil? # stripe_customer_id on Card object + np_card = nonprofit.active_card + billing_subscription = nonprofit.billing_subscription + return { json: { error: 'We don\'t have a subscription for your non-profit. Please contact support.' }, status: :unprocessable_entity } if np_card.nil? || billing_subscription.nil? # stripe_customer_id on Card object # Cancel and delete the subscription on Stripe begin @@ -21,15 +21,15 @@ module CancelBillingSubscription stripe_subscription = customer.subscriptions.retrieve(billing_subscription.stripe_subscription_id) s = stripe_subscription.delete(at_period_end: false) rescue Stripe::StripeError => e - return {json: {error: "Oops! There was an error processing your subscription cancellation. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error processing your subscription cancellation. Error: #{e}" }, status: :unprocessable_entity } end billing_plan_id = Settings.default_bp.id - billing_subscription.update_attributes({ + billing_subscription.update_attributes( billing_plan_id: billing_plan_id, status: 'active' - }) + ) - return {json:{}, status: :ok} - end + { json: {}, status: :ok } + end end diff --git a/lib/chunked_uploader/s3.rb b/lib/chunked_uploader/s3.rb index 579c6bac..9ef69015 100644 --- a/lib/chunked_uploader/s3.rb +++ b/lib/chunked_uploader/s3.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ChunkedUploader class S3 @@ -9,18 +11,18 @@ module ChunkedUploader # @param [Enumerable] chunk_enum an enumerable of strings. # @param [String] path the path to the object on your S3 bucket # @returns the url to your uploaded file - def self.upload(path,chunk_enum, metadata={}) + def self.upload(path, chunk_enum, metadata = {}) s3 = AWS::S3.new bucket = s3.buckets[S3_BUCKET_NAME] object = bucket.objects[path] io = StringIO.new('', 'w') - content_type = metadata[:content_type] ? metadata[:content_type] : nil - content_disposition = metadata[:content_disposition] ? metadata[:content_disposition] : nil + content_type = metadata[:content_type] || nil + content_disposition = metadata[:content_disposition] || nil begin - object.multipart_upload(:acl => :public_read, :content_type => content_type, content_disposition: content_disposition) do |upload| - chunk_enum.each do |chunk| + object.multipart_upload(acl: :public_read, content_type: content_type, content_disposition: content_disposition) do |upload| + chunk_enum.each do |chunk| export_returned = io.write(chunk) - if (io.size >= MINIMUMBUFFER_SIZE) + if io.size >= MINIMUMBUFFER_SIZE upload.add_part(io.string) io.reopen('') end @@ -28,10 +30,10 @@ module ChunkedUploader upload.add_part(io.string) end object.public_url.to_s - rescue => e + rescue StandardError => e io.close raise e end end end -end \ No newline at end of file +end diff --git a/lib/construct/construct_billing_subscription.rb b/lib/construct/construct_billing_subscription.rb index 51941ed4..06627966 100644 --- a/lib/construct/construct_billing_subscription.rb +++ b/lib/construct/construct_billing_subscription.rb @@ -1,22 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'stripe' require 'active_support/core_ext' module ConstructBillingSubscription + def self.with_stripe(np, billing_plan) + raise ArgumentError, 'Billing plan not found' if billing_plan.nil? - def self.with_stripe(np, billing_plan) - raise ArgumentError.new("Billing plan not found") if billing_plan.nil? - trial_end = QueryBillingSubscriptions.currently_in_trial?(np.id) ? (np.created_at + 15.days).to_i : nil - customer = Stripe::Customer.retrieve np.active_card.stripe_customer_id - stripe_subscription = customer.subscriptions.create({ - plan: billing_plan.stripe_plan_id, - trial_end: trial_end - }) - return { - billing_plan_id: billing_plan.id, - stripe_subscription_id: stripe_subscription.id, - status: stripe_subscription.status - } - end - + trial_end = QueryBillingSubscriptions.currently_in_trial?(np.id) ? (np.created_at + 15.days).to_i : nil + customer = Stripe::Customer.retrieve np.active_card.stripe_customer_id + stripe_subscription = customer.subscriptions.create( + plan: billing_plan.stripe_plan_id, + trial_end: trial_end + ) + { + billing_plan_id: billing_plan.id, + stripe_subscription_id: stripe_subscription.id, + status: stripe_subscription.status + } + end end diff --git a/lib/construct/construct_nonprofit.rb b/lib/construct/construct_nonprofit.rb index 7fa5d263..b06bdfae 100644 --- a/lib/construct/construct_nonprofit.rb +++ b/lib/construct/construct_nonprofit.rb @@ -1,17 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'create/stripe/create_stripe_account' module ConstructNonprofit - - def self.construct(user, h) - h[:verification_status] = 'unverified' - h[:published] = true - h[:statement] = h[:name][0..16] - h.except!(:website) if h[:website].blank? - stripe_acct = CreateStripeAccount.for_nonprofit(user, h) - h[:stripe_account_id] = stripe_acct.id - return h - end - + def self.construct(user, h) + h[:verification_status] = 'unverified' + h[:published] = true + h[:statement] = h[:name][0..16] + h.except!(:website) if h[:website].blank? + stripe_acct = CreateStripeAccount.for_nonprofit(user, h) + h[:stripe_account_id] = stripe_acct.id + h + end end - diff --git a/lib/controllers/campaign_helper.rb b/lib/controllers/campaign_helper.rb index d7a568a0..50b64faf 100644 --- a/lib/controllers/campaign_helper.rb +++ b/lib/controllers/campaign_helper.rb @@ -1,23 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Controllers::CampaignHelper - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper -private + private - def current_campaign - @campaign ||= FetchCampaign.with_params params, current_nonprofit - raise ActionController::RoutingError.new "Campaign not found" if @campaign.nil? - return @campaign - end + def current_campaign + @campaign ||= FetchCampaign.with_params params, current_nonprofit + raise ActionController::RoutingError, 'Campaign not found' if @campaign.nil? - def current_campaign_editor? - !params[:preview] && (current_nonprofit_user? || current_role?(:campaign_editor, current_campaign.id) || current_role?(:super_admin)) - end + @campaign + end - def authenticate_campaign_editor! - unless current_campaign_editor? - block_with_sign_in 'You need to be a campaign editor to do that.' - end - end + def current_campaign_editor? + !params[:preview] && (current_nonprofit_user? || current_role?(:campaign_editor, current_campaign.id) || current_role?(:super_admin)) + end + def authenticate_campaign_editor! + unless current_campaign_editor? + block_with_sign_in 'You need to be a campaign editor to do that.' + end + end end diff --git a/lib/controllers/event_helper.rb b/lib/controllers/event_helper.rb index a58c778f..7125436a 100644 --- a/lib/controllers/event_helper.rb +++ b/lib/controllers/event_helper.rb @@ -1,27 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Controllers::EventHelper - include Controllers::NonprofitHelper + include Controllers::NonprofitHelper -private + private - def current_event_admin? - current_nonprofit_admin? - end + def current_event_admin? + current_nonprofit_admin? + end - def current_event_editor? - !params[:preview] && (current_nonprofit_user? || current_role?(:event_editor, current_event.id) || current_role?(:super_admin)) - end + def current_event_editor? + !params[:preview] && (current_nonprofit_user? || current_role?(:event_editor, current_event.id) || current_role?(:super_admin)) + end - def authenticate_event_editor! - unless current_event_editor? - block_with_sign_in 'You need to be the event organizer or a nonprofit administrator before doing that.' - end - end + def authenticate_event_editor! + unless current_event_editor? + block_with_sign_in 'You need to be the event organizer or a nonprofit administrator before doing that.' + end + end - def current_event - @event ||= FetchEvent.with_params params, current_nonprofit - raise ActionController::RoutingError.new "Event not found" if @event.nil? - return @event - end + def current_event + @event ||= FetchEvent.with_params params, current_nonprofit + raise ActionController::RoutingError, 'Event not found' if @event.nil? + @event + end end diff --git a/lib/controllers/nonprofit_helper.rb b/lib/controllers/nonprofit_helper.rb index a62cf078..2da72bb5 100644 --- a/lib/controllers/nonprofit_helper.rb +++ b/lib/controllers/nonprofit_helper.rb @@ -1,59 +1,59 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Controllers::NonprofitHelper + private -private + def authenticate_nonprofit_user! + block_with_sign_in 'Please sign in' unless current_nonprofit_user? + end - def authenticate_nonprofit_user! - unless current_nonprofit_user? - block_with_sign_in 'Please sign in' - end - end + def authenticate_nonprofit_admin! + block_with_sign_in 'Please sign in' unless current_nonprofit_admin? + end - def authenticate_nonprofit_admin! - unless current_nonprofit_admin? - block_with_sign_in 'Please sign in' - end - end + def authenticate_min_nonprofit_plan(plan_tier) + unless current_nonprofit_user? && current_plan_tier >= plan_tier + block_with_sign_in 'Please sign in' + end + end - def authenticate_min_nonprofit_plan plan_tier - unless current_nonprofit_user? && current_plan_tier >= plan_tier - block_with_sign_in 'Please sign in' - end - end - - def current_nonprofit_user? + def current_nonprofit_user? return false if params[:preview] - return false unless current_nonprofit_without_exception - @current_user_role ||= current_role?([:nonprofit_admin, :nonprofit_associate], current_nonprofit_without_exception.id) || current_role?(:super_admin) - end + return false unless current_nonprofit_without_exception - def current_nonprofit_admin? - return false if !current_user || current_user.roles.empty? - @current_admin_role ||= current_role?(:nonprofit_admin, current_nonprofit.id) || current_role?(:super_admin) - end + @current_user_role ||= current_role?(%i[nonprofit_admin nonprofit_associate], current_nonprofit_without_exception.id) || current_role?(:super_admin) + end - def current_nonprofit - @nonprofit = current_nonprofit_without_exception - raise ActionController::RoutingError.new "Nonprofit not found" if @nonprofit.nil? - return @nonprofit - end + def current_nonprofit_admin? + return false if !current_user || current_user.roles.empty? - def current_nonprofit_without_exception - key = "current_nonprofit_#{current_user_id}_params_#{[params[:state_code], params[:city], params[:name], params[:nonprofit_id], params[:id]].join("_")}" + @current_admin_role ||= current_role?(:nonprofit_admin, current_nonprofit.id) || current_role?(:super_admin) + end + + def current_nonprofit + @nonprofit = current_nonprofit_without_exception + raise ActionController::RoutingError, 'Nonprofit not found' if @nonprofit.nil? + + @nonprofit + end + + def current_nonprofit_without_exception + key = "current_nonprofit_#{current_user_id}_params_#{[params[:state_code], params[:city], params[:name], params[:nonprofit_id], params[:id]].join('_')}" FetchNonprofit.with_params params, administered_nonprofit - end + end - def donation_stub - return current_nonprofit_without_exception.donations.last unless current_nonprofit_without_exception.donations.empty? - OpenStruct.new( - amount: 2000, - created_at: Time.zone.now, - nonprofit: current_nonprofit_without_exception, - campaign: nil, - designation: "Donor's designation here", - dedication: "Donor's dedication here", - id: 1 - ) - end + def donation_stub + return current_nonprofit_without_exception.donations.last unless current_nonprofit_without_exception.donations.empty? + OpenStruct.new( + amount: 2000, + created_at: Time.zone.now, + nonprofit: current_nonprofit_without_exception, + campaign: nil, + designation: "Donor's designation here", + dedication: "Donor's dedication here", + id: 1 + ) + end end diff --git a/lib/copy_naming_algorithm.rb b/lib/copy_naming_algorithm.rb index 729c7eba..ab8373d1 100644 --- a/lib/copy_naming_algorithm.rb +++ b/lib/copy_naming_algorithm.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CopyNamingAlgorithm DEFAULT_MAX_LENGTH = 255 DEFAULT_MAX_COPIES = 255 - DEFAULT_SEPARATOR_BEFORE_COPY_NUMBER = "_" + DEFAULT_SEPARATOR_BEFORE_COPY_NUMBER = '_' def copy_addition raise NotImplementedError @@ -20,7 +22,7 @@ class CopyNamingAlgorithm DEFAULT_MAX_COPIES end - def get_already_used_name_entities(base_name) + def get_already_used_name_entities(_base_name) raise NotImplementedError end @@ -30,36 +32,35 @@ class CopyNamingAlgorithm def create_copy_name(name_to_copy) # remove copy addition and number - base_name = name_to_copy.gsub(/#{Regexp.quote(self.copy_addition)}(#{Regexp.quote(separator_before_copy_number)}\d+)?\z/,'') - name_entities_to_check_against = get_already_used_name_entities(base_name).collect{|entity| self.get_name_for_entity(entity)}.to_a - (0..max_copies-1).each {|copy_num| + base_name = name_to_copy.gsub(/#{Regexp.quote(copy_addition)}(#{Regexp.quote(separator_before_copy_number)}\d+)?\z/, '') + name_entities_to_check_against = get_already_used_name_entities(base_name).collect { |entity| get_name_for_entity(entity) }.to_a + (0..max_copies - 1).each do |copy_num| name_to_test = generate_name(base_name, copy_num) - if (name_entities_to_check_against.none? {|entity_name| entity_name == name_to_test}) + if name_entities_to_check_against.none? { |entity_name| entity_name == name_to_test } return name_to_test end - } + end - raise UnableToCreateNameCopyError.new("It's not possible to generate a UNIQUE name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} max_copy_num: #{self.max_copies} max_length: #{self.max_length}") + raise UnableToCreateNameCopyError, "It's not possible to generate a UNIQUE name using name_to_copy: #{name_to_copy} copy_addition: #{copy_addition} separator_before_copy_number: #{separator_before_copy_number} max_copy_num: #{max_copies} max_length: #{max_length}" end def generate_name(name_to_copy, copy_num) - what_to_add = self.copy_addition + self.separator_before_copy_number + generate_copy_number(copy_num) + what_to_add = copy_addition + separator_before_copy_number + generate_copy_number(copy_num) # is what_to_add longer than max length? If so, it's not possible to create a copy - if (what_to_add.length > self.max_length) - raise UnableToCreateNameCopyError.new("It's not possible to generate a name using name_to_copy: #{name_to_copy} copy_addition: #{self.copy_addition} separator_before_copy_number: #{self.separator_before_copy_number} copy_num: #{copy_num} max_length: #{self.max_length}") + if what_to_add.length > max_length + raise UnableToCreateNameCopyError, "It's not possible to generate a name using name_to_copy: #{name_to_copy} copy_addition: #{copy_addition} separator_before_copy_number: #{separator_before_copy_number} copy_num: #{copy_num} max_length: #{max_length}" end - max_length_for_name_to_copy = self.max_length - what_to_add.length - name_to_copy[0..max_length_for_name_to_copy-1] + what_to_add + + max_length_for_name_to_copy = max_length - what_to_add.length + name_to_copy[0..max_length_for_name_to_copy - 1] + what_to_add end def generate_copy_number(unprefixed_copy_number) - number_of_digits = Math.log10(self.max_copies).floor + 1 - "%0#{number_of_digits}d" % unprefixed_copy_number + number_of_digits = Math.log10(max_copies).floor + 1 + format("%0#{number_of_digits}d", unprefixed_copy_number) end - end class UnableToCreateNameCopyError < ArgumentError - -end \ No newline at end of file +end diff --git a/lib/create/create_campaign.rb b/lib/create/create_campaign.rb index 7233d661..4a7f854c 100644 --- a/lib/create/create_campaign.rb +++ b/lib/create/create_campaign.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateCampaign CAMPAIGN_NAME_LENGTH_LIMIT = 60 - # @return [Object] a json object for historical purposes def self.create(params, nonprofit) Time.use_zone(nonprofit.timezone || 'UTC') do @@ -12,22 +13,20 @@ module CreateCampaign if !params[:campaign][:parent_campaign_id] campaign = nonprofit.campaigns.create params[:campaign] - #do notifications + # do notifications user = campaign.profile.user Role.create(name: :campaign_editor, user_id: user.id, host: self) CampaignMailer.delay.creation_followup(self) - NonprofitAdminMailer.delay.supporter_fundraiser(self) unless QueryRoles.is_nonprofit_user?(user.id, self.nonprofit_id) + NonprofitAdminMailer.delay.supporter_fundraiser(self) unless QueryRoles.is_nonprofit_user?(user.id, nonprofit_id) return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? + return campaign.as_json - #json_saved campaign, 'Campaign created! Well done.' + # json_saved campaign, 'Campaign created! Well done.' else profile_id = params[:campaign][:profile_id] Profile.find(profile_id).update_attributes params[:profile] return CreatePeerToPeerCampaign.create(params[:campaign], profile_id) end end - - - -end \ No newline at end of file +end diff --git a/lib/create/create_campaign_gift.rb b/lib/create/create_campaign_gift.rb index 17c15bbf..c1343333 100644 --- a/lib/create/create_campaign_gift.rb +++ b/lib/create/create_campaign_gift.rb @@ -1,78 +1,77 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateCampaignGift - # @param [Hash] params - # * campaign_gift_option_id - # * donation_id - def self.create(params) - ParamValidation.new(params, { - :campaign_gift_option_id => { - :required => true, - :is_integer => true - }, - :donation_id => { - :required => true, - :is_integer => true - } - }) + # @param [Hash] params + # * campaign_gift_option_id + # * donation_id + def self.create(params) + ParamValidation.new(params, + campaign_gift_option_id: { + required: true, + is_integer: true + }, + donation_id: { + required: true, + is_integer: true + }) - donation = Donation.includes(:nonprofit).includes(:supporter).includes(:recurring_donation).includes(:campaign_gifts).where('id = ?', params[:donation_id]).first - unless donation - raise ParamValidation::ValidationError.new("#{params[:donation_id]} is not a valid donation id.", {:key => :donation_id}) - end + donation = Donation.includes(:nonprofit).includes(:supporter).includes(:recurring_donation).includes(:campaign_gifts).where('id = ?', params[:donation_id]).first + unless donation + raise ParamValidation::ValidationError.new("#{params[:donation_id]} is not a valid donation id.", key: :donation_id) + end - campaign_gift_option = CampaignGiftOption.includes(:campaign).where('id = ?', params[:campaign_gift_option_id]).first - unless campaign_gift_option - raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} is not a valid campaign gift option", {:key => :campaign_gift_option_id}) - end + campaign_gift_option = CampaignGiftOption.includes(:campaign).where('id = ?', params[:campaign_gift_option_id]).first + unless campaign_gift_option + raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} is not a valid campaign gift option", key: :campaign_gift_option_id) + end - #does donation already have a campaign_gift - if (donation.campaign_gifts.any?) - raise ParamValidation::ValidationError.new("#{params[:donation_id]} already has at least one associated campaign gift", :key => :donation_id) - end + # does donation already have a campaign_gift + if donation.campaign_gifts.any? + raise ParamValidation::ValidationError.new("#{params[:donation_id]} already has at least one associated campaign gift", key: :donation_id) + end - if (donation.campaign != campaign_gift_option.campaign) - raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} is not for the same campaign as donation #{params[:donation_id]}", {:key => :campaign_gift_option_id}) - end + if donation.campaign != campaign_gift_option.campaign + raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} is not for the same campaign as donation #{params[:donation_id]}", key: :campaign_gift_option_id) + end - if ((donation.recurring_donation != nil) && (campaign_gift_option.amount_recurring != nil && campaign_gift_option.amount_recurring > 0)) - # it's a recurring_donation. Is it enough? for the gift level? - unless donation.recurring_donation.amount == (campaign_gift_option.amount_recurring) - AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) - raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} gift options requires a recurring donation of #{campaign_gift_option.amount_recurring} for donation #{donation.id}", {:key => :campaign_gift_option_id}) - end - else - unless donation.amount == (campaign_gift_option.amount_one_time) - AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) - raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} gift options requires a donation of #{campaign_gift_option.amount_one_time} for donation #{donation.id}", {:key => :campaign_gift_option_id}) - end - end + if !donation.recurring_donation.nil? && (!campaign_gift_option.amount_recurring.nil? && campaign_gift_option.amount_recurring > 0) + # it's a recurring_donation. Is it enough? for the gift level? + unless donation.recurring_donation.amount == campaign_gift_option.amount_recurring + AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) + raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} gift options requires a recurring donation of #{campaign_gift_option.amount_recurring} for donation #{donation.id}", key: :campaign_gift_option_id) + end + else + unless donation.amount == campaign_gift_option.amount_one_time + AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) + raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} gift options requires a donation of #{campaign_gift_option.amount_one_time} for donation #{donation.id}", key: :campaign_gift_option_id) + end + end - Qx.transaction do - # are any gifts available? - if campaign_gift_option.quantity.nil? || campaign_gift_option.quantity.zero?|| campaign_gift_option.total_gifts < campaign_gift_option.quantity - gift = CampaignGift.new params - gift.save! - return gift - end - end - AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) - raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} has no more inventory", {:key => :campaign_gift_option_id}) - - end - - def self.validate_campaign_gift(cg) - donation = cg.donation - campaign_gift_option = cg.campaign_gift_option - if ((donation.recurring_donation != nil) && (campaign_gift_option.amount_recurring != nil && campaign_gift_option.amount_recurring > 0)) - # it's a recurring_donation. Is it enough? for the gift level? - unless donation.recurring_donation.amount == (campaign_gift_option.amount_recurring) - raise ParamValidation::ValidationError.new("#{campaign_gift_option.id} gift options requires a recurring donation of at least #{campaign_gift_option.amount_recurring}", {:key => :campaign_gift_option_id}) - end - else - unless donation.amount == (campaign_gift_option.amount_one_time) - raise ParamValidation::ValidationError.new("#{campaign_gift_option.id} gift options requires a donation of at least #{campaign_gift_option.amount_one_time}", {:key => :campaign_gift_option_id}) - end - end - end + Qx.transaction do + # are any gifts available? + if campaign_gift_option.quantity.nil? || campaign_gift_option.quantity.zero? || campaign_gift_option.total_gifts < campaign_gift_option.quantity + gift = CampaignGift.new params + gift.save! + return gift + end + end + AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) + raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} has no more inventory", key: :campaign_gift_option_id) + end + def self.validate_campaign_gift(cg) + donation = cg.donation + campaign_gift_option = cg.campaign_gift_option + if !donation.recurring_donation.nil? && (!campaign_gift_option.amount_recurring.nil? && campaign_gift_option.amount_recurring > 0) + # it's a recurring_donation. Is it enough? for the gift level? + unless donation.recurring_donation.amount == campaign_gift_option.amount_recurring + raise ParamValidation::ValidationError.new("#{campaign_gift_option.id} gift options requires a recurring donation of at least #{campaign_gift_option.amount_recurring}", key: :campaign_gift_option_id) + end + else + unless donation.amount == campaign_gift_option.amount_one_time + raise ParamValidation::ValidationError.new("#{campaign_gift_option.id} gift options requires a donation of at least #{campaign_gift_option.amount_one_time}", key: :campaign_gift_option_id) + end + end + end end diff --git a/lib/create/create_campaign_gift_option.rb b/lib/create/create_campaign_gift_option.rb index 98ca146b..ae162e03 100644 --- a/lib/create/create_campaign_gift_option.rb +++ b/lib/create/create_campaign_gift_option.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateCampaignGiftOption - - def self.create campaign, params - gift_option = campaign.campaign_gift_options.build params - gift_option.save - return gift_option - end - + def self.create(campaign, params) + gift_option = campaign.campaign_gift_options.build params + gift_option.save + gift_option + end end diff --git a/lib/create/create_custom_field_join.rb b/lib/create/create_custom_field_join.rb index 55ebc38c..6f4e42ce 100644 --- a/lib/create/create_custom_field_join.rb +++ b/lib/create/create_custom_field_join.rb @@ -1,39 +1,38 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateCustomFieldJoin + def self.create(supporter, _profile_id, params) + custom_field = supporter.custom_field_joins.create(params) + custom_field + end - def self.create(supporter, profile_id, params) - custom_field = supporter.custom_field_joins.create(params) - return custom_field - end + # In the future, this should create an activity feed entry - # In the future, this should create an activity feed entry + # @param [Array] custom_fields Hash with following keys: + # * custom_field_master_id [Integer] for the key corresponding to custom_field_master_id + # * value [Object] the expected value of the field. If this key is an empty string, we remove the custom_field - # @param [Array] custom_fields Hash with following keys: - # * custom_field_master_id [Integer] for the key corresponding to custom_field_master_id - # * value [Object] the expected value of the field. If this key is an empty string, we remove the custom_field - - def self.modify(np, user, supporter_ids, custom_fields) - return if supporter_ids.nil? || supporter_ids.empty? - return if custom_fields.nil? || custom_fields.empty? - supporter_ids.each do |sid| - supporter = np.supporters.find(sid) - custom_fields.each do |custom_field| - existing = supporter.custom_field_joins.find_by_custom_field_master_id(custom_field[:custom_field_master_id]) - if existing - existing.update_attributes({ - custom_field_master_id: custom_field[:custom_field_master_id], - value: custom_field[:value] - }) - else - self.create(supporter, user.profile.id, { - custom_field_master_id: custom_field[:custom_field_master_id], - value: custom_field[:value] - }) - end - end - end - return np - end + def self.modify(np, user, supporter_ids, custom_fields) + return if supporter_ids.nil? || supporter_ids.empty? + return if custom_fields.nil? || custom_fields.empty? + supporter_ids.each do |sid| + supporter = np.supporters.find(sid) + custom_fields.each do |custom_field| + existing = supporter.custom_field_joins.find_by_custom_field_master_id(custom_field[:custom_field_master_id]) + if existing + existing.update_attributes( + custom_field_master_id: custom_field[:custom_field_master_id], + value: custom_field[:value] + ) + else + create(supporter, user.profile.id, + custom_field_master_id: custom_field[:custom_field_master_id], + value: custom_field[:value]) + end + end + end + np + end end - diff --git a/lib/create/create_custom_field_master.rb b/lib/create/create_custom_field_master.rb index decc7ce6..470f5aed 100644 --- a/lib/create/create_custom_field_master.rb +++ b/lib/create/create_custom_field_master.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateCustomFieldMaster - - def self.create(nonprofit, params) - custom_field_master = nonprofit.custom_field_masters.create(params) - return custom_field_master - end + def self.create(nonprofit, params) + custom_field_master = nonprofit.custom_field_masters.create(params) + custom_field_master + end end diff --git a/lib/create/create_peer_to_peer_campaign.rb b/lib/create/create_peer_to_peer_campaign.rb index 032de0ab..91d997ca 100644 --- a/lib/create/create_peer_to_peer_campaign.rb +++ b/lib/create/create_peer_to_peer_campaign.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreatePeerToPeerCampaign def self.create(campaign_params, profile_id) begin - parent_campaign = Campaign.find(campaign_params[:parent_campaign_id]) - + parent_campaign = Campaign.find(campaign_params[:parent_campaign_id]) rescue ActiveRecord::RecordNotFound return { errors: { parent_campaign_id: 'not found' } }.as_json end - p2p_params = campaign_params.except(:nonprofit_id, :summary,:goal_amount) + p2p_params = campaign_params.except(:nonprofit_id, :summary, :goal_amount) p2p_params.merge!(parent_campaign.child_params) profile = Profile.find(profile_id) @@ -22,9 +23,21 @@ module CreatePeerToPeerCampaign campaign.profile = profile campaign.save - campaign.update_attribute(:main_image, parent_campaign.main_image) unless !parent_campaign.main_image rescue AWS::S3::Errors::NoSuchKey - campaign.update_attribute(:background_image, parent_campaign.background_image) unless !parent_campaign.background_image rescue AWS::S3::Errors::NoSuchKey - campaign.update_attribute(:banner_image, parent_campaign.banner_image) unless !parent_campaign.banner_image rescue AWS::S3::Errors::NoSuchKey + begin + campaign.update_attribute(:main_image, parent_campaign.main_image) if parent_campaign.main_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end + begin + campaign.update_attribute(:background_image, parent_campaign.background_image) if parent_campaign.background_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end + begin + campaign.update_attribute(:banner_image, parent_campaign.banner_image) if parent_campaign.banner_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? diff --git a/lib/create/create_tag_master.rb b/lib/create/create_tag_master.rb index 7c8fc139..9eef5b94 100644 --- a/lib/create/create_tag_master.rb +++ b/lib/create/create_tag_master.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module CreateTagMaster - - def self.create(nonprofit, params) - tag_master = nonprofit.tag_masters.create(params) - return tag_master - end + def self.create(nonprofit, params) + tag_master = nonprofit.tag_masters.create(params) + tag_master + end end - diff --git a/lib/create/stripe/create_stripe_account.rb b/lib/create/stripe/create_stripe_account.rb index 1d1a9412..75681c7c 100644 --- a/lib/create/stripe/create_stripe_account.rb +++ b/lib/create/stripe/create_stripe_account.rb @@ -1,37 +1,38 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'format/name' require 'get_data' require 'stripe' module CreateStripeAccount - - def self.for_nonprofit(user, params) - fst_name, lst_name = Format::Name.split_full(GetData.chain(user, :profile, :name)) - return Stripe::Account.create({ - managed: true, - email: params[:email], - business_name: params[:name], - business_url: params[:website], - legal_entity: { - type: 'company', - address: { - line1: params[:address], - city: params[:city], - state: params[:state_code], - postal_code: params[:zip_code], - country: 'US' - }, - business_name: params[:name], - business_tax_id: params[:ein], - first_name: fst_name, - last_name: lst_name - }, - product_description: 'Nonprofit donations', - tos_acceptance: { - date: Time.current.to_i, - ip: user.current_sign_in_ip - }, - transfer_schedule: { interval: 'manual' } - }) - end + def self.for_nonprofit(user, params) + fst_name, lst_name = Format::Name.split_full(GetData.chain(user, :profile, :name)) + Stripe::Account.create( + managed: true, + email: params[:email], + business_name: params[:name], + business_url: params[:website], + legal_entity: { + type: 'company', + address: { + line1: params[:address], + city: params[:city], + state: params[:state_code], + postal_code: params[:zip_code], + country: 'US' + }, + business_name: params[:name], + business_tax_id: params[:ein], + first_name: fst_name, + last_name: lst_name + }, + product_description: 'Nonprofit donations', + tos_acceptance: { + date: Time.current.to_i, + ip: user.current_sign_in_ip + }, + transfer_schedule: { interval: 'manual' } + ) + end end diff --git a/lib/cypher.rb b/lib/cypher.rb index 3f95b517..0f2e971b 100644 --- a/lib/cypher.rb +++ b/lib/cypher.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'openssl' @@ -8,31 +10,30 @@ require 'openssl' # .iv, .auth_tag both are stored with the encrypted data module Cypher - def self.encrypt(data) cipher = create_cipher cipher.encrypt cipher.key = Base64.decode64(ENV['CYPHER_KEY']) iv = cipher.random_iv encrypted = cipher.update(data) + cipher.final - return {iv: Base64.encode64(iv), key: Base64.encode64(encrypted)} + { iv: Base64.encode64(iv), key: Base64.encode64(encrypted) } end # hash must have properties for :iv and :key def self.decrypt(hash) - iv, encrypted = [Base64.decode64(hash['iv']), Base64.decode64(hash['key'])] + iv = Base64.decode64(hash['iv']) + encrypted = Base64.decode64(hash['key']) decipher = create_cipher decipher.decrypt decipher.key = Base64.decode64(ENV['CYPHER_KEY']) decipher.iv = iv - return decipher.update(encrypted) + decipher.final + decipher.update(encrypted) + decipher.final end -private + private def self.create_cipher OpenSSL::Cipher::AES256.new(:CBC) end - end diff --git a/lib/delayed_job_helper.rb b/lib/delayed_job_helper.rb index 7189a29e..f03b7c8f 100644 --- a/lib/delayed_job_helper.rb +++ b/lib/delayed_job_helper.rb @@ -1,20 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'delayed_job' module DelayedJobHelper - - # Create a serialized delayed job handler for use in inserting new delayed jobs with raw sql - # Be sure to wrap the handler in double quotes when inserting, not single - def self.create_handler(obj, method_name, args) - Delayed::PerformableMethod.new(obj, method_name, args).to_yaml.to_s - end + # Create a serialized delayed job handler for use in inserting new delayed jobs with raw sql + # Be sure to wrap the handler in double quotes when inserting, not single + def self.create_handler(obj, method_name, args) + Delayed::PerformableMethod.new(obj, method_name, args).to_yaml.to_s + end # Manually enqueue a job - def self.enqueue_job(obj, method_name, args, options={}) + def self.enqueue_job(obj, method_name, args, options = {}) handler = Delayed::PerformableMethod.new(obj, method_name, args).to_yaml.to_s Qx.insert_into(:delayed_jobs) - .values({ + .values( created_at: Time.current, updated_at: Time.current, priority: options[:priority] || 0, @@ -22,6 +23,6 @@ module DelayedJobHelper handler: handler, run_at: options[:run_at] || Time.current, queue: options[:queue] - }).returning('*').execute + ).returning('*').execute end end diff --git a/lib/delete/delete_campaign_gift_option.rb b/lib/delete/delete_campaign_gift_option.rb index bcb0afe1..249c0ef4 100644 --- a/lib/delete/delete_campaign_gift_option.rb +++ b/lib/delete/delete_campaign_gift_option.rb @@ -1,27 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module DeleteCampaignGiftOption def self.delete(campaign, campaign_gift_option_id) - ParamValidation.new({:campaign => campaign, - :campaign_gift_option_id => campaign_gift_option_id}, - { - :campaign => { - :required => true, - :is_a => Campaign - }, - :campaign_gift_option_id => { - :required => true, - :is_integer => true - } - } - ) + ParamValidation.new({ campaign: campaign, + campaign_gift_option_id: campaign_gift_option_id }, + campaign: { + required: true, + is_a: Campaign + }, + campaign_gift_option_id: { + required: true, + is_integer: true + }) Qx.transaction do cgo = campaign.campaign_gift_options.where('id = ? ', campaign_gift_option_id).first unless cgo - raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} is not a valid gift option for campaign #{campaign.id}", {:key => :campaign_gift_option_id}) + raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} is not a valid gift option for campaign #{campaign.id}", key: :campaign_gift_option_id) end - if (cgo.campaign_gifts.any?) - raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} already has campaign gifts. It can't be deleted for safety reasons.", {:key => :campaign_gift_option_id}) + if cgo.campaign_gifts.any? + raise ParamValidation::ValidationError.new("#{campaign_gift_option_id} already has campaign gifts. It can't be deleted for safety reasons.", key: :campaign_gift_option_id) end cgo.destroy @@ -29,4 +28,4 @@ module DeleteCampaignGiftOption return cgo end end -end \ No newline at end of file +end diff --git a/lib/delete/delete_custom_field_joins.rb b/lib/delete/delete_custom_field_joins.rb index ac3e8080..9e392733 100644 --- a/lib/delete/delete_custom_field_joins.rb +++ b/lib/delete/delete_custom_field_joins.rb @@ -1,39 +1,37 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module DeleteCustomFieldJoins - @columns = ['id', 'custom_field_master_id', 'supporter_id', 'value', 'created_at', 'updated_at', 'metadata'] + @columns = %w[id custom_field_master_id supporter_id value created_at updated_at metadata] def self.find_multiple_custom_field_joins - bad_results = Qx.select("CONCAT(custom_field_joins.supporter_id, '_', custom_field_joins.custom_field_master_id) AS our_concat, COUNT(id) AS our_count"). - from(:custom_field_joins). - group_by("our_concat"). - having('COUNT(id) > 1').parse - - - custom_field_joins_from_qx = CustomFieldJoin. - where("CONCAT(custom_field_joins.supporter_id, '_', custom_field_joins.custom_field_master_id) IN (SELECT our_concat FROM (#{bad_results}) AS ignore)"). - select('id, custom_field_master_id, supporter_id, created_at, updated_at') - grouped_custom_field_joins = custom_field_joins_from_qx.group_by{|tj| "#{tj.supporter_id}_#{tj.custom_field_master_id}"} + bad_results = Qx.select("CONCAT(custom_field_joins.supporter_id, '_', custom_field_joins.custom_field_master_id) AS our_concat, COUNT(id) AS our_count") + .from(:custom_field_joins) + .group_by('our_concat') + .having('COUNT(id) > 1').parse + custom_field_joins_from_qx = CustomFieldJoin + .where("CONCAT(custom_field_joins.supporter_id, '_', custom_field_joins.custom_field_master_id) IN (SELECT our_concat FROM (#{bad_results}) AS ignore)") + .select('id, custom_field_master_id, supporter_id, created_at, updated_at') + grouped_custom_field_joins = custom_field_joins_from_qx.group_by { |tj| "#{tj.supporter_id}_#{tj.custom_field_master_id}" } ids_to_delete = [] - grouped_custom_field_joins.each { |_, v| - - sorted = v.sort_by {|a| a.updated_at }.to_a - ids_to_delete += sorted.map{|i| i.id}[0, sorted.count - 1] - } + grouped_custom_field_joins.each do |_, v| + sorted = v.sort_by(&:updated_at).to_a + ids_to_delete += sorted.map(&:id)[0, sorted.count - 1] + end ids_to_delete end def self.copy_and_delete(ids_to_delete) if ids_to_delete.any? - Qx.insert_into(:custom_field_joins_backup, @columns).select(@columns).from(:custom_field_joins).where('id IN ($ids)', ids:ids_to_delete).execute + Qx.insert_into(:custom_field_joins_backup, @columns).select(@columns).from(:custom_field_joins).where('id IN ($ids)', ids: ids_to_delete).execute CustomFieldJoin.where('id IN (?)', ids_to_delete).delete_all end - end def self.revert Qx.insert_into(:custom_field_joins, @columns).select(@columns).from(:custom_field_joins_backup).execute - Qx.execute_raw("DELETE FROM custom_field_joins_backup") + Qx.execute_raw('DELETE FROM custom_field_joins_backup') end -end \ No newline at end of file +end diff --git a/lib/delete/delete_tag_joins.rb b/lib/delete/delete_tag_joins.rb index 72e347d5..0e0c875d 100644 --- a/lib/delete/delete_tag_joins.rb +++ b/lib/delete/delete_tag_joins.rb @@ -1,25 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module DeleteTagJoins - @columns = ['id', 'created_at', 'updated_at', 'metadata', 'tag_master_id', 'supporter_id'] + @columns = %w[id created_at updated_at metadata tag_master_id supporter_id] def self.find_multiple_tag_joins - qx_results = Qx.select("CONCAT(tag_joins.supporter_id, '_', tag_joins.tag_master_id) AS our_concat, COUNT(id) AS our_count"). - from(:tag_joins). - group_by("our_concat"). - having('COUNT(id) > 1'). - execute - - tag_joins_from_qx = TagJoin.where("CONCAT(supporter_id, '_', tag_master_id) IN (?)", qx_results.map{|i| i["our_concat"] }).select('id, tag_master_id, supporter_id, created_at') - grouped_tagged_joins = tag_joins_from_qx.group_by{|tj| "#{tj.supporter_id}_#{tj.tag_master_id}"} + qx_results = Qx.select("CONCAT(tag_joins.supporter_id, '_', tag_joins.tag_master_id) AS our_concat, COUNT(id) AS our_count") + .from(:tag_joins) + .group_by('our_concat') + .having('COUNT(id) > 1') + .execute + tag_joins_from_qx = TagJoin.where("CONCAT(supporter_id, '_', tag_master_id) IN (?)", qx_results.map { |i| i['our_concat'] }).select('id, tag_master_id, supporter_id, created_at') + grouped_tagged_joins = tag_joins_from_qx.group_by { |tj| "#{tj.supporter_id}_#{tj.tag_master_id}" } ids_to_delete = [] - grouped_tagged_joins.each { |_, v| - - sorted = v.sort_by {|a| a.created_at }.to_a - ids_to_delete += sorted.map{|i| i.id}[0, sorted.count - 1] - } + grouped_tagged_joins.each do |_, v| + sorted = v.sort_by(&:created_at).to_a + ids_to_delete += sorted.map(&:id)[0, sorted.count - 1] + end ids_to_delete end @@ -27,18 +27,17 @@ module DeleteTagJoins def self.copy_and_delete(ids_to_delete) if ids_to_delete.any? - #select_query = Qx.select(@columns).from(:tag_joins).where('id IN ($ids)', ids:ids_to_delete).parse + # select_query = Qx.select(@columns).from(:tag_joins).where('id IN ($ids)', ids:ids_to_delete).parse - Qx.insert_into(:tag_joins_backup, @columns).select(@columns).from(:tag_joins).where('id IN ($ids)', ids:ids_to_delete).execute - # Qx.execute_raw("INSERT INTO tag_joins_backup ('id', '' #{select_query}") + Qx.insert_into(:tag_joins_backup, @columns).select(@columns).from(:tag_joins).where('id IN ($ids)', ids: ids_to_delete).execute + # Qx.execute_raw("INSERT INTO tag_joins_backup ('id', '' #{select_query}") TagJoin.where('id IN (?)', ids_to_delete).delete_all end - end def self.revert Qx.insert_into(:tag_joins, @columns).select(@columns).from(:tag_joins_backup).execute - #Qx.execute_raw("INSERT INTO tag_joins SELECT * FROM tag_joins_backup") - Qx.execute_raw("DELETE FROM tag_joins_backup") + # Qx.execute_raw("INSERT INTO tag_joins SELECT * FROM tag_joins_backup") + Qx.execute_raw('DELETE FROM tag_joins_backup') end -end \ No newline at end of file +end diff --git a/lib/email.rb b/lib/email.rb index d255f6e6..255c722f 100644 --- a/lib/email.rb +++ b/lib/email.rb @@ -1,7 +1,7 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Email - - Regex ||= /\A[^ ]+@[^ ]+\.[^ ]+/i - #PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$' - + Regex ||= /\A[^ ]+@[^ ]+\.[^ ]+/i.freeze + # PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$' end diff --git a/lib/email_job_queue.rb b/lib/email_job_queue.rb index 25482a03..84e247bd 100644 --- a/lib/email_job_queue.rb +++ b/lib/email_job_queue.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module EmailJobQueue def self.queue(klass, *args) Delayed::Job.enqueue klass.new(*args) end -end \ No newline at end of file +end diff --git a/lib/errors/authentication_error.rb b/lib/errors/authentication_error.rb index 603bff36..01f51b35 100644 --- a/lib/errors/authentication_error.rb +++ b/lib/errors/authentication_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class AuthenticationError < RuntimeError -end \ No newline at end of file +end diff --git a/lib/errors/cc_org_error.rb b/lib/errors/cc_org_error.rb index 0d1dff0e..9dcadea5 100644 --- a/lib/errors/cc_org_error.rb +++ b/lib/errors/cc_org_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CCOrgError < RuntimeError -end \ No newline at end of file +end diff --git a/lib/errors/charge_error.rb b/lib/errors/charge_error.rb index 33ddb2c1..7f9160e0 100644 --- a/lib/errors/charge_error.rb +++ b/lib/errors/charge_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ChargeError < RuntimeError -end \ No newline at end of file +end diff --git a/lib/errors/expired_token_error.rb b/lib/errors/expired_token_error.rb index 066c32d4..2fa14242 100644 --- a/lib/errors/expired_token_error.rb +++ b/lib/errors/expired_token_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ExpiredTokenError < RuntimeError -end \ No newline at end of file +end diff --git a/lib/errors/not_enough_quantity_error.rb b/lib/errors/not_enough_quantity_error.rb index d9f74792..018d5ffe 100644 --- a/lib/errors/not_enough_quantity_error.rb +++ b/lib/errors/not_enough_quantity_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NotEnoughQuantityError < CCOrgError attr_accessor :klass, :id, :requested @@ -7,4 +9,4 @@ class NotEnoughQuantityError < CCOrgError @requested = requested super(msg) end -end \ No newline at end of file +end diff --git a/lib/export/export_payments.rb b/lib/export/export_payments.rb index a54d5dc1..1324aab4 100644 --- a/lib/export/export_payments.rb +++ b/lib/export/export_payments.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ExportPayments - def self.initiate_export(npo_id, params, user_id) - ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id }, npo_id: { required: true, is_integer: true }, params: { required: true, is_hash: true }, @@ -12,6 +12,7 @@ module ExportPayments unless npo raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) @@ -30,7 +31,7 @@ module ExportPayments user_id: { required: true, is_integer: true }, export_id: { required: true, is_integer: true }) - params = JSON.parse(params, :object_class=> HashWithIndifferentAccess) + params = JSON.parse(params, object_class: HashWithIndifferentAccess) # verify that it's also a hash since we can't do that at once ParamValidation.new({ params: params }, params: { is_hash: true }) @@ -45,30 +46,29 @@ module ExportPayments unless Nonprofit.exists?(npo_id) raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) end - file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') + file_date = Time.now.getutc.strftime('%m-%d-%Y--%H-%M-%S') filename = "tmp/csv-exports/payments-#{file_date}.csv" - url = CHUNKED_UPLOADER.upload(filename, QueryPayments.for_export_enumerable(npo_id, params, 30000).map{|i| i.to_csv}, :content_type => 'text/csv', content_disposition: 'attachment') + url = CHUNKED_UPLOADER.upload(filename, QueryPayments.for_export_enumerable(npo_id, params, 30_000).map(&:to_csv), content_type: 'text/csv', content_disposition: 'attachment') export.url = url export.status = :completed export.ended = Time.now export.save! ExportMailer.delay.export_payments_completed_notification(export) - rescue => e + rescue StandardError => e if export export.status = :failed export.exception = e.to_s export.ended = Time.now export.save! - if user - ExportMailer.delay.export_payments_failed_notification(export) - end + ExportMailer.delay.export_payments_failed_notification(export) if user raise e end raise e diff --git a/lib/export/export_recurring_donations.rb b/lib/export/export_recurring_donations.rb index a8b146e5..9d9e1d23 100644 --- a/lib/export/export_recurring_donations.rb +++ b/lib/export/export_recurring_donations.rb @@ -1,9 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ExportRecurringDonations - def self.initiate_export(npo_id, params, user_id) - ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id }, npo_id: { required: true, is_integer: true }, params: { required: true, is_hash: true }, @@ -12,6 +12,7 @@ module ExportRecurringDonations unless npo raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) @@ -30,7 +31,7 @@ module ExportRecurringDonations user_id: { required: true, is_integer: true }, export_id: { required: true, is_integer: true }) - params = JSON.parse(params, :object_class=> HashWithIndifferentAccess) + params = JSON.parse(params, object_class: HashWithIndifferentAccess) # verify that it's also a hash since we can't do that at once ParamValidation.new({ params: params }, params: { is_hash: true }) @@ -45,22 +46,23 @@ module ExportRecurringDonations unless Nonprofit.exists?(npo_id) raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) end - file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') + file_date = Time.now.getutc.strftime('%m-%d-%Y--%H-%M-%S') filename = "tmp/csv-exports/recurring_donations-#{file_date}.csv" - url = CHUNKED_UPLOADER.upload(filename, QueryRecurringDonations.for_export_enumerable(npo_id, params, 30000).map{|i| i.to_csv}, :content_type => 'text/csv', content_disposition: 'attachment') + url = CHUNKED_UPLOADER.upload(filename, QueryRecurringDonations.for_export_enumerable(npo_id, params, 30_000).map(&:to_csv), content_type: 'text/csv', content_disposition: 'attachment') export.url = url export.status = :completed export.ended = Time.now export.save! ExportMailer.delay.export_recurring_donations_completed_notification(export) - rescue => e + rescue StandardError => e if export export.status = :failed export.exception = e.to_s diff --git a/lib/export/export_supporter_notes.rb b/lib/export/export_supporter_notes.rb index 83b9316e..52b3af11 100644 --- a/lib/export/export_supporter_notes.rb +++ b/lib/export/export_supporter_notes.rb @@ -1,74 +1,77 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ExportSupporterNotes - def self.initiate_export(npo_id, params, user_id) + def self.initiate_export(npo_id, params, user_id) + ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id }, + npo_id: { required: true, is_integer: true }, + params: { required: true, is_hash: true }, + user_id: { required: true, is_integer: true }) + npo = Nonprofit.where('id = ?', npo_id).first + unless npo + raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) + end - ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id }, - npo_id: { required: true, is_integer: true }, - params: { required: true, is_hash: true }, - user_id: { required: true, is_integer: true }) - npo = Nonprofit.where('id = ?', npo_id).first - unless npo - raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) - end - user = User.where('id = ?', user_id).first - unless user - raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) - end - - e = Export.create(nonprofit: npo, user: user, status: :queued, export_type: 'ExportSupporterNotes', parameters: params.to_json) - - DelayedJobHelper.enqueue_job(ExportSupporterNotes, :run_export, [npo_id, params.to_json, user_id, e.id]) + user = User.where('id = ?', user_id).first + unless user + raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) + end + + e = Export.create(nonprofit: npo, user: user, status: :queued, export_type: 'ExportSupporterNotes', parameters: params.to_json) + + DelayedJobHelper.enqueue_job(ExportSupporterNotes, :run_export, [npo_id, params.to_json, user_id, e.id]) + end + + def self.run_export(npo_id, params, user_id, export_id) + # need to check that + ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id, export_id: export_id }, + npo_id: { required: true, is_integer: true }, + params: { required: true, is_json: true }, + user_id: { required: true, is_integer: true }, + export_id: { required: true, is_integer: true }) + + params = JSON.parse(params, object_class: HashWithIndifferentAccess) + # verify that it's also a hash since we can't do that at once + ParamValidation.new({ params: params }, + params: { is_hash: true }) + begin + export = Export.find(export_id) + rescue ActiveRecord::RecordNotFound + raise ParamValidation::ValidationError.new("Export #{export_id} doesn't exist!", key: :export_id) + end + export.status = :started + export.save! + + unless Nonprofit.exists?(npo_id) + raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) + end + + user = User.where('id = ?', user_id).first + unless user + raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) + end + + file_date = Time.now.getutc.strftime('%m-%d-%Y--%H-%M-%S') + filename = "tmp/csv-exports/supporters-notes-#{file_date}.csv" + + url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.supporter_note_export_enumerable(npo_id, params, 30_000).map(&:to_csv), content_type: 'text/csv', content_disposition: 'attachment') + export.url = url + export.status = :completed + export.ended = Time.now + export.save! + + EmailJobQueue.queue(JobTypes::ExportSupporterNotesCompletedJob, export) + rescue StandardError => e + if export + export.status = :failed + export.exception = e.to_s + export.ended = Time.now + export.save! + if user + EmailJobQueue.queue(JobTypes::ExportSupporterNotesFailedJob, export) end - - def self.run_export(npo_id, params, user_id, export_id) - # need to check that - ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id, export_id: export_id }, - npo_id: { required: true, is_integer: true }, - params: { required: true, is_json: true }, - user_id: { required: true, is_integer: true }, - export_id: { required: true, is_integer: true }) - - params = JSON.parse(params, :object_class=> HashWithIndifferentAccess) - # verify that it's also a hash since we can't do that at once - ParamValidation.new({ params: params }, - params: { is_hash: true }) - begin - export = Export.find(export_id) - rescue ActiveRecord::RecordNotFound - raise ParamValidation::ValidationError.new("Export #{export_id} doesn't exist!", key: :export_id) - end - export.status = :started - export.save! - - unless Nonprofit.exists?(npo_id) - raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) - end - user = User.where('id = ?', user_id).first - unless user - raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) - end - - file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') - filename = "tmp/csv-exports/supporters-notes-#{file_date}.csv" - - url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.supporter_note_export_enumerable(npo_id, params, 30000).map{|i| i.to_csv}, content_type: 'text/csv', content_disposition: 'attachment') - export.url = url - export.status = :completed - export.ended = Time.now - export.save! - - EmailJobQueue.queue(JobTypes::ExportSupporterNotesCompletedJob, export) - rescue => e - if export - export.status = :failed - export.exception = e.to_s - export.ended = Time.now - export.save! - if user - EmailJobQueue.queue(JobTypes::ExportSupporterNotesFailedJob, export) - end - raise e - end - raise e - end -end \ No newline at end of file + raise e + end + raise e + end +end diff --git a/lib/export/export_supporters.rb b/lib/export/export_supporters.rb index c657ffb0..8c7c4991 100644 --- a/lib/export/export_supporters.rb +++ b/lib/export/export_supporters.rb @@ -1,6 +1,7 @@ +# frozen_string_literal: true + module ExportSupporters def self.initiate_export(npo_id, params, user_id) - ParamValidation.new({ npo_id: npo_id, params: params, user_id: user_id }, npo_id: { required: true, is_integer: true }, params: { required: true, is_hash: true }, @@ -9,6 +10,7 @@ module ExportSupporters unless npo raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) @@ -27,7 +29,7 @@ module ExportSupporters user_id: { required: true, is_integer: true }, export_id: { required: true, is_integer: true }) - params = JSON.parse(params, :object_class=> HashWithIndifferentAccess) + params = JSON.parse(params, object_class: HashWithIndifferentAccess) # verify that it's also a hash since we can't do that at once ParamValidation.new({ params: params }, params: { is_hash: true }) @@ -42,32 +44,31 @@ module ExportSupporters unless Nonprofit.exists?(npo_id) raise ParamValidation::ValidationError.new("Nonprofit #{npo_id} doesn't exist!", key: :npo_id) end + user = User.where('id = ?', user_id).first unless user raise ParamValidation::ValidationError.new("User #{user_id} doesn't exist!", key: :user_id) end - file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') + file_date = Time.now.getutc.strftime('%m-%d-%Y--%H-%M-%S') filename = "tmp/csv-exports/supporters-#{file_date}.csv" - url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.for_export_enumerable(npo_id, params, 30000).map{|i| i.to_csv}, content_type: 'text/csv', content_disposition: 'attachment') + url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.for_export_enumerable(npo_id, params, 30_000).map(&:to_csv), content_type: 'text/csv', content_disposition: 'attachment') export.url = url export.status = :completed export.ended = Time.now export.save! - EmailJobQueue.queue(JobTypes::ExportSupportersCompletedJob, export) - rescue => e + EmailJobQueue.queue(JobTypes::ExportSupportersCompletedJob, export) + rescue StandardError => e if export export.status = :failed export.exception = e.to_s export.ended = Time.now export.save! - if user - EmailJobQueue.queue(JobTypes::ExportSupportersFailedJob, export) - end + EmailJobQueue.queue(JobTypes::ExportSupportersFailedJob, export) if user raise e end raise e end -end \ No newline at end of file +end diff --git a/lib/fetch/fetch_background_image.rb b/lib/fetch/fetch_background_image.rb index 79492cbd..5c3a7e72 100644 --- a/lib/fetch/fetch_background_image.rb +++ b/lib/fetch/fetch_background_image.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchBackgroundImage - - def self.with_model(model) - return model.background_image_url(:normal) unless model.background_image.file.nil? - end + def self.with_model(model) + return model.background_image_url(:normal) unless model.background_image.file.nil? + end end diff --git a/lib/fetch/fetch_campaign.rb b/lib/fetch/fetch_campaign.rb index 52bd775e..508a9afc 100644 --- a/lib/fetch/fetch_campaign.rb +++ b/lib/fetch/fetch_campaign.rb @@ -1,14 +1,13 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchCampaign - - def self.with_params(params, nonprofit=nil) - nonprofit ||= FetchNonprofit.with_params(params) - if params[:campaign_slug] - return nonprofit.campaigns.where(slug: params[:campaign_slug]).last - elsif params[:campaign_id] || params[:id] - return nonprofit.campaigns.find(params[:campaign_id] || params[:id]) - end - end - + def self.with_params(params, nonprofit = nil) + nonprofit ||= FetchNonprofit.with_params(params) + if params[:campaign_slug] + return nonprofit.campaigns.where(slug: params[:campaign_slug]).last + elsif params[:campaign_id] || params[:id] + return nonprofit.campaigns.find(params[:campaign_id] || params[:id]) + end + end end - diff --git a/lib/fetch/fetch_coupon.rb b/lib/fetch/fetch_coupon.rb index 402a4e16..c9cb9af9 100644 --- a/lib/fetch/fetch_coupon.rb +++ b/lib/fetch/fetch_coupon.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchCoupon - def self.page params - return params[:name].gsub('-','_') if params[:name] - end -end \ No newline at end of file + def self.page(params) + return params[:name].tr('-', '_') if params[:name] + end +end diff --git a/lib/fetch/fetch_event.rb b/lib/fetch/fetch_event.rb index cf8a1a9c..0b501881 100644 --- a/lib/fetch/fetch_event.rb +++ b/lib/fetch/fetch_event.rb @@ -1,14 +1,13 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchEvent - - def self.with_params(params, nonprofit=nil) - nonprofit ||= FetchNonprofit.with_params(params) - if params[:event_slug] - return nonprofit.events.find_by_slug(params[:event_slug]) - elsif params[:event_id] || params[:id] - return nonprofit.events.find(params[:event_id] || params[:id]) - end - end - + def self.with_params(params, nonprofit = nil) + nonprofit ||= FetchNonprofit.with_params(params) + if params[:event_slug] + return nonprofit.events.find_by_slug(params[:event_slug]) + elsif params[:event_id] || params[:id] + return nonprofit.events.find(params[:event_id] || params[:id]) + end + end end - diff --git a/lib/fetch/fetch_miscellaneous_np_info.rb b/lib/fetch/fetch_miscellaneous_np_info.rb index 85510589..8bac4187 100644 --- a/lib/fetch/fetch_miscellaneous_np_info.rb +++ b/lib/fetch/fetch_miscellaneous_np_info.rb @@ -1,8 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchMiscellaneousNpInfo def self.fetch(np_id) - ParamValidation.new({np_id: np_id}, np_id: {:required => true, :is_integer => true}) - raise ParamValidation::ValidationError.new("Nonprofit #{np_id} does not exist", {key: :np_id}) unless Nonprofit.exists?(np_id) + ParamValidation.new({ np_id: np_id }, np_id: { required: true, is_integer: true }) + raise ParamValidation::ValidationError.new("Nonprofit #{np_id} does not exist", key: :np_id) unless Nonprofit.exists?(np_id) + MiscellaneousNpInfo.where('nonprofit_id = ?', np_id).first_or_initialize end -end \ No newline at end of file +end diff --git a/lib/fetch/fetch_nonprofit.rb b/lib/fetch/fetch_nonprofit.rb index a7043742..32db3546 100644 --- a/lib/fetch/fetch_nonprofit.rb +++ b/lib/fetch/fetch_nonprofit.rb @@ -1,15 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchNonprofit - - def self.with_params(params, administered_nonprofit=nil) - if params[:state_code] && params[:city] && params[:name] - return Nonprofit.where(:state_code_slug => params[:state_code], :city_slug => params[:city], :slug => params[:name]).last - elsif params[:nonprofit_id] || params[:id] - return Nonprofit.find_by_id(params[:nonprofit_id] || params[:id]) + def self.with_params(params, administered_nonprofit = nil) + if params[:state_code] && params[:city] && params[:name] + Nonprofit.where(state_code_slug: params[:state_code], city_slug: params[:city], slug: params[:name]).last + elsif params[:nonprofit_id] || params[:id] + Nonprofit.find_by_id(params[:nonprofit_id] || params[:id]) elsif administered_nonprofit administered_nonprofit - end - end - + end + end end - diff --git a/lib/fetch/fetch_nonprofit_email.rb b/lib/fetch/fetch_nonprofit_email.rb index dc11e78d..0dbd4e72 100644 --- a/lib/fetch/fetch_nonprofit_email.rb +++ b/lib/fetch/fetch_nonprofit_email.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchNonprofitEmail + def self.with_charge(charge) + nonprofit = charge.nonprofit + nonprofit.email.blank? ? Settings.mailer.email : nonprofit.email + end - def self.with_charge charge - nonprofit = charge.nonprofit - nonprofit.email.blank? ? Settings.mailer.email : nonprofit.email - end - - def self.with_donation donation - nonprofit = donation.nonprofit - nonprofit.email.blank? ? Settings.mailer.email : nonprofit.email - end + def self.with_donation(donation) + nonprofit = donation.nonprofit + nonprofit.email.blank? ? Settings.mailer.email : nonprofit.email + end end diff --git a/lib/fetch/fetch_todo_status.rb b/lib/fetch/fetch_todo_status.rb index ac601198..245bcf12 100644 --- a/lib/fetch/fetch_todo_status.rb +++ b/lib/fetch/fetch_todo_status.rb @@ -1,28 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module FetchTodoStatus + def self.for_profile(np) + { + has_logo: np.logo?, + has_background: np.background_image?, + has_summary: np.summary?, + has_image: np.main_image?, + has_highlight: !np.achievements.join.blank?, + has_services: np.full_description? + } + end - def self.for_profile(np) - { - has_logo: np.logo?, - has_background: np.background_image?, - has_summary: np.summary?, - has_image: np.main_image?, - has_highlight: !np.achievements.join.blank?, - has_services: np.full_description? - } - end - - def self.for_dashboard(np) - { - has_campaign: np.campaigns.any?, - has_event: np.events.any?, - has_donation: np.donations.any?, - has_branding: np.brand_color?, - has_bank: np.bank_account.present?, - is_paying: np.billing_plan.present?, - has_imported: np.supporters.pluck(:imported_at).any?, - is_verified: np.verification_status == 'verified' && np.bank_account.present?, - has_thank_you: np.thank_you_note.present? - } - end + def self.for_dashboard(np) + { + has_campaign: np.campaigns.any?, + has_event: np.events.any?, + has_donation: np.donations.any?, + has_branding: np.brand_color?, + has_bank: np.bank_account.present?, + is_paying: np.billing_plan.present?, + has_imported: np.supporters.pluck(:imported_at).any?, + is_verified: np.verification_status == 'verified' && np.bank_account.present?, + has_thank_you: np.thank_you_note.present? + } + end end diff --git a/lib/fetch/stripe/fetch_stripe_account.rb b/lib/fetch/stripe/fetch_stripe_account.rb index 0e7f8aa3..f7cffa4e 100644 --- a/lib/fetch/stripe/fetch_stripe_account.rb +++ b/lib/fetch/stripe/fetch_stripe_account.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Retrive a stripe account object, catching any errors module FetchStripeAccount - - def self.with_account_id(stripe_account_id) - begin - stripe_acct = Stripe::Account.retrieve(stripe_account_id) - rescue - stripe_acct = nil - end - return stripe_acct - end - + def self.with_account_id(stripe_account_id) + begin + stripe_acct = Stripe::Account.retrieve(stripe_account_id) + rescue StandardError + stripe_acct = nil + end + stripe_acct + end end diff --git a/lib/format/format/address.rb b/lib/format/format/address.rb index 62781dec..1cf070d6 100644 --- a/lib/format/format/address.rb +++ b/lib/format/format/address.rb @@ -1,23 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Format; module Address - - def self.full_address(street, city, state, zip=nil) - # Albuquerque | NM | Albuquerque NM | 1234 Street Ln, Albuquerque NM - [[street, city].compact.join(", "), state, zip].compact.join(' ') - end +module Format + module Address + def self.full_address(street, city, state, zip = nil) + # Albuquerque | NM | Albuquerque NM | 1234 Street Ln, Albuquerque NM + [[street, city].compact.join(', '), state, zip].compact.join(' ') + end - def self.city_and_state(city,state) - [city, state].join(', ') if !city.blank? && !state.blank? - end + def self.city_and_state(city, state) + [city, state].join(', ') if !city.blank? && !state.blank? + end - def self.city_or_state(city,state) - city_and_state(city,state) || city || state - end + def self.city_or_state(city, state) + city_and_state(city, state) || city || state + end - def self.with_supporter(s) - return '' if s.nil? - [[s.address, s.city, s.state_code].reject(&:blank?).join(", "), s.zip_code].reject(&:blank?).join(" ") - end + def self.with_supporter(s) + return '' if s.nil? + [[s.address, s.city, s.state_code].reject(&:blank?).join(', '), s.zip_code].reject(&:blank?).join(' ') + end end; end - diff --git a/lib/format/format/csv.rb b/lib/format/format/csv.rb index 1eda6751..c107cbd4 100644 --- a/lib/format/format/csv.rb +++ b/lib/format/format/csv.rb @@ -1,33 +1,33 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'csv' require 'format/currency' module Format module Csv - # Convert an array of hashes of data into a csv # @param [Array] an array of hashes. The hash keys of the first item in the array become the CSV titles # @return [String] def self.from_data(arr) - return CSV.generate do |csv| - csv << arr.first.keys.map{|k| k.to_s.titleize} - arr.each{|h| csv << h.values} + CSV.generate do |csv| + csv << arr.first.keys.map { |k| k.to_s.titleize } + arr.each { |h| csv << h.values } end end def self.from_vectors(vecs) - return CSV.generate do |csv| - csv << vecs.first.to_a.map{|k| k.to_s.titleize} - vecs.drop(1).each{|v| csv << v.to_a} + CSV.generate do |csv| + csv << vecs.first.to_a.map { |k| k.to_s.titleize } + vecs.drop(1).each { |v| csv << v.to_a } end end def self.from_array(arr) - return CSV.generate do |csv| - csv << arr.first.map{|h| h.to_s.titleize} - arr.drop(1).each{|row| csv << (row||[])} + CSV.generate do |csv| + csv << arr.first.map { |h| h.to_s.titleize } + arr.drop(1).each { |row| csv << (row || []) } end end - end end diff --git a/lib/format/format/currency.rb b/lib/format/format/currency.rb index e81e1c01..e8161e3b 100644 --- a/lib/format/format/currency.rb +++ b/lib/format/format/currency.rb @@ -1,21 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Format; module Currency - - - # Converts currency units into subunits. - # @param [String] units - # @return [Integer] - def self.dollars_to_cents(units) - return (units.gsub(',','').gsub(Settings.intntl.currencies[0],'').to_f * 100).to_i - end - - # Converts currency subunits into units. - # @param [Integer] subunits - # @return [String] - def self.cents_to_dollars(subunits) - return (subunits.to_f / 100.0).to_s - .gsub(/^(\d+)\.0$/, '\1') # remove trailing zero if no decimals (eg. "1.0" -> "1") - .gsub(/^(\d+)\.(\d)$/, '\1.\20') # add a second zero if single decimal (eg. "9.9" -> "9.90") - end +module Format + module Currency + # Converts currency units into subunits. + # @param [String] units + # @return [Integer] + def self.dollars_to_cents(units) + (units.delete(',').gsub(Settings.intntl.currencies[0], '').to_f * 100).to_i + end + # Converts currency subunits into units. + # @param [Integer] subunits + # @return [String] + def self.cents_to_dollars(subunits) + (subunits.to_f / 100.0).to_s + .gsub(/^(\d+)\.0$/, '\1') # remove trailing zero if no decimals (eg. "1.0" -> "1") + .gsub(/^(\d+)\.(\d)$/, '\1.\20') # add a second zero if single decimal (eg. "9.9" -> "9.90") + end end; end diff --git a/lib/format/format/date.rb b/lib/format/format/date.rb index 7d803e3c..77c57ee2 100644 --- a/lib/format/format/date.rb +++ b/lib/format/format/date.rb @@ -1,64 +1,69 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'chronic' -module Format; module Date +module Format + module Date + ISORegex = /\d\d\d\d-\d\d-\d\d/.freeze - ISORegex = /\d\d\d\d-\d\d-\d\d/ - - def self.parse(str) - Chronic.parse(str) - end - - def self.from(str) - return DateTime.strptime(str, "%m/%d/%Y") - end - - def self.to_readable(date) - date.strftime("%A, %B #{date.day.ordinalize}") - end - - def self.full(date, timezone=nil) - return '' if date.nil? - date = Chronic.parse(date) if date.is_a?(String) - date = date.in_time_zone(timezone) if timezone - date.strftime("%m/%-d/%Y %l:%M%P") - end - - def self.full_range(date1, date2, timezone=nil) - return full(date1, timezone) if date2.nil? - return full(date2, timezone) if date1.nil? - if simple(date1) == simple(date2) - return full(date1, timezone) + ' - ' + time(date2, timezone) - else - return full(date1, timezone) + ' - ' + full(date2, timezone) + def self.parse(str) + Chronic.parse(str) end - end - def self.simple(date, timezone=nil) - return '' if date.nil? - date = Chronic.parse(date) if date.is_a?(String) - date = date.in_time_zone(timezone) if timezone - date.strftime("%m/%d/%Y") - end + def self.from(str) + DateTime.strptime(str, '%m/%d/%Y') + end - def self.time(datetime, timezone=nil) - return '' if datetime.nil? - datetime = Chronic.parse(datetime) if datetime.is_a?(String) - datetime = datetime.in_time_zone(timezone) if timezone - datetime.strftime("%l:%M%P") - end + def self.to_readable(date) + date.strftime("%A, %B #{date.day.ordinalize}") + end - def self.us_timezones - #zones=ActiveSupport::TimeZone.us_zones - zones=ActiveSupport::TimeZone.all - names = zones.map(&:name) - vals = zones.map{|t| t.tzinfo.name} - return names.zip(vals).sort_by{|name, val| name} - end + def self.full(date, timezone = nil) + return '' if date.nil? - def self.parse_partial_str(str) - return nil if str.nil? - Time.new(*str.match(/(\d\d\d\d)-?(\d\d)?-?(\d\d)?/).to_a[1..-1].compact.map(&:to_i)) - end + date = Chronic.parse(date) if date.is_a?(String) + date = date.in_time_zone(timezone) if timezone + date.strftime('%m/%-d/%Y %l:%M%P') + end + def self.full_range(date1, date2, timezone = nil) + return full(date1, timezone) if date2.nil? + return full(date2, timezone) if date1.nil? + if simple(date1) == simple(date2) + return full(date1, timezone) + ' - ' + time(date2, timezone) + else + return full(date1, timezone) + ' - ' + full(date2, timezone) + end + end + + def self.simple(date, timezone = nil) + return '' if date.nil? + + date = Chronic.parse(date) if date.is_a?(String) + date = date.in_time_zone(timezone) if timezone + date.strftime('%m/%d/%Y') + end + + def self.time(datetime, timezone = nil) + return '' if datetime.nil? + + datetime = Chronic.parse(datetime) if datetime.is_a?(String) + datetime = datetime.in_time_zone(timezone) if timezone + datetime.strftime('%l:%M%P') + end + + def self.us_timezones + # zones=ActiveSupport::TimeZone.us_zones + zones = ActiveSupport::TimeZone.all + names = zones.map(&:name) + vals = zones.map { |t| t.tzinfo.name } + names.zip(vals).sort_by { |name, _val| name } + end + + def self.parse_partial_str(str) + return nil if str.nil? + + Time.new(*str.match(/(\d\d\d\d)-?(\d\d)?-?(\d\d)?/).to_a[1..-1].compact.map(&:to_i)) + end end; end diff --git a/lib/format/format/dedication.rb b/lib/format/format/dedication.rb index 29650c18..86d6c388 100644 --- a/lib/format/format/dedication.rb +++ b/lib/format/format/dedication.rb @@ -1,16 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'json' module Format module Dedication - def self.from_json(json_text) begin hash = JSON.parse(json_text) - rescue + rescue StandardError return json_text end - return "Donation made in #{hash['type'] || 'honor'} of #{hash['name']}. Note: #{hash['note']}" + "Donation made in #{hash['type'] || 'honor'} of #{hash['name']}. Note: #{hash['note']}" end end end diff --git a/lib/format/format/geography.rb b/lib/format/format/geography.rb index 6ac3394c..f53f1532 100644 --- a/lib/format/format/geography.rb +++ b/lib/format/format/geography.rb @@ -1,305 +1,307 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Format; module Geography +module Format + module Geography + StateCodes = %w[AL AK AZ AR CA CO CT DE DC FL GA HI ID IL IN IA KS KY LA ME MD MA MI MN MS MO MT NE NV NH NJ NM NY NC ND OH OK OR PA PR RI SC SD TN TX UT VT VA WA WV WI WY].freeze - StateCodes = [ 'AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', 'FL', 'GA', 'HI', 'ID', 'IL', 'IN', 'IA', 'KS', 'KY', 'LA', 'ME', 'MD', 'MA', 'MI', 'MN', 'MS', 'MO', 'MT', 'NE', 'NV', 'NH', 'NJ', 'NM', 'NY', 'NC', 'ND', 'OH', 'OK', 'OR', 'PA', 'PR', 'RI', 'SC', 'SD', 'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY' ] + StateMappings = { + 'alabama' => 'AL', + 'alaska' => 'AK', + 'arizona' => 'AZ', + 'arkansas' => 'AR', + 'california' => 'CA', + 'colorado' => 'CO', + 'connecticut' => 'CT', + 'delaware' => 'DE', + 'district of columbia' => 'DC', + 'florida' => 'FL', + 'georgia' => 'GA', + 'hawaii' => 'HI', + 'idaho' => 'ID', + 'illinois' => 'IL', + 'indiana' => 'IN', + 'iowa' => 'IA', + 'kansas' => 'KS', + 'kentucky' => 'KY', + 'louisiana' => 'LA', + 'maine' => 'ME', + 'maryland' => 'MD', + 'massachusetts' => 'MA', + 'michigan' => 'MI', + 'minnesota' => 'MN', + 'mississippi' => 'MS', + 'missouri' => 'MO', + 'montana' => 'MT', + 'nebraska' => 'NE', + 'nevada' => 'NV', + 'new hampshire' => 'NH', + 'new jersey' => 'NJ', + 'new mexico' => 'NM', + 'new york' => 'NY', + 'north carolina' => 'NC', + 'north dakota' => 'ND', + 'ohio' => 'OH', + 'oklahoma' => 'OK', + 'oregon' => 'OR', + 'pennsylvania' => 'PA', + 'puerto rico' => 'PR', + 'rhode island' => 'RI', + 'south carolina' => 'SC', + 'south dakota' => 'SD', + 'tennessee' => 'TN', + 'texas' => 'TX', + 'utah' => 'UT', + 'vermont' => 'VT', + 'virginia' => 'VA', + 'washington' => 'WA', + 'west virginia' => 'WV', + 'wisconsin' => 'WI', + 'wyoming' => 'WY' + }.freeze - StateMappings = { - 'alabama' => 'AL', - 'alaska' => 'AK', - 'arizona' => 'AZ', - 'arkansas' => 'AR', - 'california' => 'CA', - 'colorado' => 'CO', - 'connecticut' => 'CT', - 'delaware' => 'DE', - 'district of columbia' => 'DC', - 'florida' => 'FL', - 'georgia' => 'GA', - 'hawaii' => 'HI', - 'idaho' => 'ID', - 'illinois' => 'IL', - 'indiana' => 'IN', - 'iowa' => 'IA', - 'kansas' => 'KS', - 'kentucky' => 'KY', - 'louisiana' => 'LA', - 'maine' => 'ME', - 'maryland' => 'MD', - 'massachusetts' => 'MA', - 'michigan' => 'MI', - 'minnesota' => 'MN', - 'mississippi' => 'MS', - 'missouri' => 'MO', - 'montana' => 'MT', - 'nebraska' => 'NE', - 'nevada' => 'NV', - 'new hampshire' => 'NH', - 'new jersey' => 'NJ', - 'new mexico' => 'NM', - 'new york' => 'NY', - 'north carolina' => 'NC', - 'north dakota' => 'ND', - 'ohio' => 'OH', - 'oklahoma' => 'OK', - 'oregon' => 'OR', - 'pennsylvania' => 'PA', - 'puerto rico' => 'PR', - 'rhode island' => 'RI', - 'south carolina' => 'SC', - 'south dakota' => 'SD', - 'tennessee' => 'TN', - 'texas' => 'TX', - 'utah' => 'UT', - 'vermont' => 'VT', - 'virginia' => 'VA', - 'washington' => 'WA', - 'west virginia' => 'WV', - 'wisconsin' => 'WI', - 'wyoming' => 'WY' - } + Countries = [ + 'Afghanistan', + 'Albania', + 'Algeria', + 'American Samoa', + 'Andorra', + 'Angola', + 'Anguilla', + 'Antarctica', + 'Antigua and Barbuda', + 'Argentina', + 'Armenia', + 'Aruba', + 'Australia', + 'Austria', + 'Azerbaijan', + 'Bahamas', + 'Bahrain', + 'Bangladesh', + 'Barbados', + 'Belarus', + 'Belgium', + 'Belize', + 'Benin', + 'Bermuda', + 'Bhutan', + 'Bolivia', + 'Bosnia and Herzegovina', + 'Botswana', + 'Bouvet Island', + 'Brazil', + 'British Indian Ocean Territory', + 'Brunei Darussalam', + 'Bulgaria', + 'Burkina Faso', + 'Burundi', + 'Cambodia', + 'Cameroon', + 'Canada', + 'Cape Verde', + 'Cayman Islands', + 'Central African Republic', + 'Chad', + 'Chile', + 'China', + 'Christmas Island', + 'Cocos (Keeling) Islands', + 'Colombia', + 'Comoros', + 'Congo', + 'Cook Islands', + 'Costa Rica', + "Cote D'ivoire", + 'Croatia', + 'Cuba', + 'Cyprus', + 'Czech Republic', + 'Denmark', + 'Djibouti', + 'Dominica', + 'Dominican Republic', + 'Ecuador', + 'Egypt', + 'El Salvador', + 'Equatorial Guinea', + 'Eritrea', + 'Estonia', + 'Ethiopia', + 'Falkland Islands (Malvinas)', + 'Faroe Islands', + 'Fiji', + 'Finland', + 'France', + 'French Guiana', + 'French Polynesia', + 'French Southern Territories', + 'Gabon', + 'Gambia', + 'Georgia', + 'Germany', + 'Ghana', + 'Gibraltar', + 'Greece', + 'Greenland', + 'Grenada', + 'Guadeloupe', + 'Guam', + 'Guatemala', + 'Guinea', + 'Guinea-bissau', + 'Guyana', + 'Haiti', + 'Heard Island and Mcdonald Islands', + 'Honduras', + 'Hong Kong', + 'Hungary', + 'Iceland', + 'India', + 'Indonesia', + 'Iran', + 'Iraq', + 'Ireland', + 'Israel', + 'Italy', + 'Jamaica', + 'Japan', + 'Jordan', + 'Kazakhstan', + 'Kenya', + 'Kiribati', + 'Korea (South)', + 'Kuwait', + 'Kyrgyzstan', + "Lao People's Democratic Republic", + 'Latvia', + 'Lebanon', + 'Lesotho', + 'Liberia', + 'Libyan Arab Jamahiriya', + 'Liechtenstein', + 'Lithuania', + 'Luxembourg', + 'Macao', + 'Macedonia', + 'Madagascar', + 'Malawi', + 'Malaysia', + 'Maldives', + 'Mali', + 'Malta', + 'Marshall Islands', + 'Martinique', + 'Mauritania', + 'Mauritius', + 'Mayotte', + 'Mexico', + 'Micronesia', + 'Moldova', + 'Monaco', + 'Mongolia', + 'Montserrat', + 'Morocco', + 'Mozambique', + 'Myanmar', + 'Namibia', + 'Nauru', + 'Nepal', + 'Netherlands', + 'Netherlands Antilles', + 'New Caledonia', + 'New Zealand', + 'Nicaragua', + 'Niger', + 'Nigeria', + 'Niue', + 'Norfolk Island', + 'Northern Mariana Islands', + 'Norway', + 'Oman', + 'Pakistan', + 'Palau', + 'Palestinian Territory', + 'Panama', + 'Papua New Guinea', + 'Paraguay', + 'Peru', + 'Philippines', + 'Pitcairn', + 'Poland', + 'Portugal', + 'Puerto Rico', + 'Qatar', + 'Reunion', + 'Romania', + 'Russia', + 'Rwanda', + 'Saint Helena', + 'Saint Kitts and Nevis', + 'Saint Lucia', + 'Saint Pierre and Miquelon', + 'Saint Vincent and The Grenadines', + 'Samoa', + 'San Marino', + 'Sao Tome and Principe', + 'Saudi Arabia', + 'Senegal', + 'Serbia and Montenegro', + 'Seychelles', + 'Sierra Leone', + 'Singapore', + 'Slovakia', + 'Slovenia', + 'Solomon Islands', + 'Somalia', + 'South Africa', + 'South Georgia and The South Sandwich Islands', + 'Spain', + 'Sri Lanka', + 'Sudan', + 'Suriname', + 'Svalbard and Jan Mayen', + 'Swaziland', + 'Sweden', + 'Switzerland', + 'Syria ', + 'Taiwan', + 'Tajikistan', + 'Tanzania', + 'Thailand', + 'Timor-leste', + 'Togo', + 'Tokelau', + 'Tonga', + 'Trinidad and Tobago', + 'Tunisia', + 'Turkey', + 'Turkmenistan', + 'Tuvalu', + 'Uganda', + 'Ukraine', + 'United Arab Emirates', + 'United Kingdom', + 'United States', + 'Uruguay', + 'Uzbekistan', + 'Vanuatu', + 'Venezuela', + 'Viet Nam', + 'Virgin Islands', + 'Wallis and Futuna', + 'Western Sahara', + 'Yemen', + 'Zambia', + 'Zimbabwe' + ].freeze -Countries = [ - "Afghanistan", - "Albania", - "Algeria", - "American Samoa", - "Andorra", - "Angola", - "Anguilla", - "Antarctica", - "Antigua and Barbuda", - "Argentina", - "Armenia", - "Aruba", - "Australia", - "Austria", - "Azerbaijan", - "Bahamas", - "Bahrain", - "Bangladesh", - "Barbados", - "Belarus", - "Belgium", - "Belize", - "Benin", - "Bermuda", - "Bhutan", - "Bolivia", - "Bosnia and Herzegovina", - "Botswana", - "Bouvet Island", - "Brazil", - "British Indian Ocean Territory", - "Brunei Darussalam", - "Bulgaria", - "Burkina Faso", - "Burundi", - "Cambodia", - "Cameroon", - "Canada", - "Cape Verde", - "Cayman Islands", - "Central African Republic", - "Chad", - "Chile", - "China", - "Christmas Island", - "Cocos (Keeling) Islands", - "Colombia", - "Comoros", - "Congo", - "Cook Islands", - "Costa Rica", - "Cote D'ivoire", - "Croatia", - "Cuba", - "Cyprus", - "Czech Republic", - "Denmark", - "Djibouti", - "Dominica", - "Dominican Republic", - "Ecuador", - "Egypt", - "El Salvador", - "Equatorial Guinea", - "Eritrea", - "Estonia", - "Ethiopia", - "Falkland Islands (Malvinas)", - "Faroe Islands", - "Fiji", - "Finland", - "France", - "French Guiana", - "French Polynesia", - "French Southern Territories", - "Gabon", - "Gambia", - "Georgia", - "Germany", - "Ghana", - "Gibraltar", - "Greece", - "Greenland", - "Grenada", - "Guadeloupe", - "Guam", - "Guatemala", - "Guinea", - "Guinea-bissau", - "Guyana", - "Haiti", - "Heard Island and Mcdonald Islands", - "Honduras", - "Hong Kong", - "Hungary", - "Iceland", - "India", - "Indonesia", - "Iran", - "Iraq", - "Ireland", - "Israel", - "Italy", - "Jamaica", - "Japan", - "Jordan", - "Kazakhstan", - "Kenya", - "Kiribati", - "Korea (South)", - "Kuwait", - "Kyrgyzstan", - "Lao People's Democratic Republic", - "Latvia", - "Lebanon", - "Lesotho", - "Liberia", - "Libyan Arab Jamahiriya", - "Liechtenstein", - "Lithuania", - "Luxembourg", - "Macao", - "Macedonia", - "Madagascar", - "Malawi", - "Malaysia", - "Maldives", - "Mali", - "Malta", - "Marshall Islands", - "Martinique", - "Mauritania", - "Mauritius", - "Mayotte", - "Mexico", - "Micronesia", - "Moldova", - "Monaco", - "Mongolia", - "Montserrat", - "Morocco", - "Mozambique", - "Myanmar", - "Namibia", - "Nauru", - "Nepal", - "Netherlands", - "Netherlands Antilles", - "New Caledonia", - "New Zealand", - "Nicaragua", - "Niger", - "Nigeria", - "Niue", - "Norfolk Island", - "Northern Mariana Islands", - "Norway", - "Oman", - "Pakistan", - "Palau", - "Palestinian Territory", - "Panama", - "Papua New Guinea", - "Paraguay", - "Peru", - "Philippines", - "Pitcairn", - "Poland", - "Portugal", - "Puerto Rico", - "Qatar", - "Reunion", - "Romania", - "Russia", - "Rwanda", - "Saint Helena", - "Saint Kitts and Nevis", - "Saint Lucia", - "Saint Pierre and Miquelon", - "Saint Vincent and The Grenadines", - "Samoa", - "San Marino", - "Sao Tome and Principe", - "Saudi Arabia", - "Senegal", - "Serbia and Montenegro", - "Seychelles", - "Sierra Leone", - "Singapore", - "Slovakia", - "Slovenia", - "Solomon Islands", - "Somalia", - "South Africa", - "South Georgia and The South Sandwich Islands", - "Spain", - "Sri Lanka", - "Sudan", - "Suriname", - "Svalbard and Jan Mayen", - "Swaziland", - "Sweden", - "Switzerland", - "Syria ", - "Taiwan", - "Tajikistan", - "Tanzania", - "Thailand", - "Timor-leste", - "Togo", - "Tokelau", - "Tonga", - "Trinidad and Tobago", - "Tunisia", - "Turkey", - "Turkmenistan", - "Tuvalu", - "Uganda", - "Ukraine", - "United Arab Emirates", - "United Kingdom", - "United States", - "Uruguay", - "Uzbekistan", - "Vanuatu", - "Venezuela", - "Viet Nam", - "Virgin Islands", - "Wallis and Futuna", - "Western Sahara", - "Yemen", - "Zambia", - "Zimbabwe" -] - - # Convert a full state name like "New Mexico" into a code like "NM" - # Will leave strings that are already state codes alone - def self.full_state_to_code(str) - str = str.strip - return str if StateCodes.include?(str.upcase) - return StateMappings[str.downcase] - end + # Convert a full state name like "New Mexico" into a code like "NM" + # Will leave strings that are already state codes alone + def self.full_state_to_code(str) + str = str.strip + return str if StateCodes.include?(str.upcase) + StateMappings[str.downcase] + end end; end diff --git a/lib/format/format/html.rb b/lib/format/format/html.rb index 23f2a543..8d2e5906 100644 --- a/lib/format/format/html.rb +++ b/lib/format/format/html.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Format module HTML def self.has_only_empty_tags(html_str) - return true if html_str && html_str.gsub(/<[^>]*>/ui,'').gsub(" ", "").strip == "" + return true if html_str && html_str.gsub(/<[^>]*>/ui, '').gsub(' ', '').strip == '' end end end diff --git a/lib/format/format/indefinitize.rb b/lib/format/format/indefinitize.rb index 68683734..e84fd2d9 100644 --- a/lib/format/format/indefinitize.rb +++ b/lib/format/format/indefinitize.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Format - module Indefinitize - VOWELS = %w(a e i o u) + module Indefinitize + VOWELS = %w[a e i o u].freeze - def self.article word - VOWELS.include?(word[0].downcase) ? 'an' : 'a' - end + def self.article(word) + VOWELS.include?(word[0].downcase) ? 'an' : 'a' + end - def self.with_article word - article(word) + ' ' + word - end - end + def self.with_article(word) + article(word) + ' ' + word + end + end end diff --git a/lib/format/format/interpolate.rb b/lib/format/format/interpolate.rb index dc53a5a7..16e1d3b9 100644 --- a/lib/format/format/interpolate.rb +++ b/lib/format/format/interpolate.rb @@ -1,9 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Format module Interpolate def self.with_hash(str, hash) return '' if str.nil? - str.gsub(/{{.+}}/){|key| hash[key.gsub(/[{}]/,'')]} + + str.gsub(/{{.+}}/) { |key| hash[key.gsub(/[{}]/, '')] } end end end diff --git a/lib/format/format/name.rb b/lib/format/format/name.rb index 53d4dccb..fea72fb2 100644 --- a/lib/format/format/name.rb +++ b/lib/format/format/name.rb @@ -1,17 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'active_support/core_ext' module Format - module Name - - def self.split_full(name) - return '' if name.nil? - name.split(/\ (\w+\s*)$/) - end + module Name + def self.split_full(name) + return '' if name.nil? + + name.split(/\ (\w+\s*)$/) + end # Format a nonprofit name into an email header def self.email_from_np(np_name) - "\"#{np_name.gsub(',', '').gsub("\"", '')}\" <#{Settings.mailer.email}>" + "\"#{np_name.delete(',').delete('"')}\" <#{Settings.mailer.email}>" end - end + end end diff --git a/lib/format/format/phone.rb b/lib/format/format/phone.rb index 81e88201..f20e21b4 100644 --- a/lib/format/format/phone.rb +++ b/lib/format/format/phone.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Format; module Phone - - def self.readable(number) - # Convert to: - # (505) 263-6320 - # or: - # 263-6320 - return '' if number.blank? - - stripped = number.gsub(/[-\(\)\.\s]/, '') # remove extra chars and space - if stripped.length == 10 - return "(#{stripped[0..2]}) #{stripped[3..5]}-#{stripped[6..9]}" - elsif stripped.length == 7 - return "#{stripped[0..2]}-#{stripped[3..6]}" - else - return number - end - end +module Format + module Phone + def self.readable(number) + # Convert to: + # (505) 263-6320 + # or: + # 263-6320 + return '' if number.blank? + stripped = number.gsub(/[-\(\)\.\s]/, '') # remove extra chars and space + if stripped.length == 10 + return "(#{stripped[0..2]}) #{stripped[3..5]}-#{stripped[6..9]}" + elsif stripped.length == 7 + return "#{stripped[0..2]}-#{stripped[3..6]}" + else + return number + end + end end; end - diff --git a/lib/format/format/remove_diacritics.rb b/lib/format/format/remove_diacritics.rb index 52680e0c..67ba2c74 100644 --- a/lib/format/format/remove_diacritics.rb +++ b/lib/format/format/remove_diacritics.rb @@ -1,16 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require "i18n" +require 'i18n' module Format - module RemoveDiacritics - - def self.from_hash(hash, keys) - # returns a new hash with any diacritics replaced with a plain character + module RemoveDiacritics + def self.from_hash(hash, keys) + # returns a new hash with any diacritics replaced with a plain character # only from values corresponding to specified keys: - # {"city" => "São Paulo"} ["city"] will return {"city" => "Sao Paulo"} - Hash[hash.map{|k, v| [k, (keys.include? k) ? I18n.transliterate(v) : v]}] - end - - end + # {"city" => "São Paulo"} ["city"] will return {"city" => "Sao Paulo"} + Hash[hash.map { |k, v| [k, (keys.include? k) ? I18n.transliterate(v) : v] }] + end + end end - diff --git a/lib/format/format/timezone.rb b/lib/format/format/timezone.rb index 08cec6dc..2fac11f6 100644 --- a/lib/format/format/timezone.rb +++ b/lib/format/format/timezone.rb @@ -1,25 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Format module Timezone def self.to_proxy(str) - dict = { - "Hawaii" => 'Pacific/Honolulu', - "Alaska" => 'America/Juneau', - "Pacific Time (US & Canada)" => 'America/Los_Angeles', - "Arizona" => 'America/Phoenix', - "Mountain Time (US & Canada)" => 'America/Denver', - "Central Time (US & Canada)" => 'America/Chicago', - "Eastern Time (US & Canada)" => 'America/New_York', - "Indiana (East)" => 'America/Indiana/Indianapolis' + dict = { + 'Hawaii' => 'Pacific/Honolulu', + 'Alaska' => 'America/Juneau', + 'Pacific Time (US & Canada)' => 'America/Los_Angeles', + 'Arizona' => 'America/Phoenix', + 'Mountain Time (US & Canada)' => 'America/Denver', + 'Central Time (US & Canada)' => 'America/Chicago', + 'Eastern Time (US & Canada)' => 'America/New_York', + 'Indiana (East)' => 'America/Indiana/Indianapolis' } - if dict.has_key?(str) + if dict.key?(str) return dict[str] - elsif dict.has_value?(str) + elsif dict.value?(str) return str else - return false + return false end end end end - diff --git a/lib/format/format/url.rb b/lib/format/format/url.rb index 59a1fe42..1ff81c6f 100644 --- a/lib/format/format/url.rb +++ b/lib/format/format/url.rb @@ -1,27 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Format; module Url +module Format + module Url + def self.without_prefix(url) + url.gsub(%r{(http(s)?://)|(www\.)|(\?.*$)|(#.*$)}, '') + end - def self.without_prefix(url) - url.gsub(/(http(s)?:\/\/)|(www\.)|(\?.*$)|(#.*$)/, '') - end + # Given ["What hello", "hi! lol?"] + # Return ["what-hello", "hi-lol"] + def self.convert_to_slug(*words) + return '' if words.empty? || !words.all? # true if any are nil or empty - # Given ["What hello", "hi! lol?"] - # Return ["what-hello", "hi-lol"] - def self.convert_to_slug(*words) - return '' if words.empty? || !words.all? # true if any are nil or empty - words.map do |d| - d.strip.downcase - .gsub(/['`]/,'') # no apostrophes - .gsub(/\./,'') # no dots - .gsub(/\s*@\s*/, ' at ') # @ -> at - .gsub(/\s*&\s*/, ' and ') # & -> and - .gsub(/\s*[^A-Za-z0-9\.\-]\s*/, '-') # replace oddballs with hyphen - .gsub(/\A[-\.]+|[-\.]+\z/,'') # strip leading/trailing hyphens - end.join("/") - end - - def self.concat(*urls) - return urls.join('/').gsub(/([^:])\/\/+/,'\1/') - end + words.map do |d| + d.strip.downcase + .gsub(/['`]/, '') # no apostrophes + .delete('.') # no dots + .gsub(/\s*@\s*/, ' at ') # @ -> at + .gsub(/\s*&\s*/, ' and ') # & -> and + .gsub(/\s*[^A-Za-z0-9\.\-]\s*/, '-') # replace oddballs with hyphen + .gsub(/\A[-\.]+|[-\.]+\z/, '') # strip leading/trailing hyphens + end.join('/') + end + def self.concat(*urls) + urls.join('/').gsub(%r{([^:])//+}, '\1/') + end end; end diff --git a/lib/generators/api/entity/entity_generator.rb b/lib/generators/api/entity/entity_generator.rb index 8e781872..1382f313 100644 --- a/lib/generators/api/entity/entity_generator.rb +++ b/lib/generators/api/entity/entity_generator.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails/generators' class Api::EntityGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_to_entity - template 'entity.rb.erb', File.join("app/api/houdini/v1/entities", "#{name.underscore}.rb") + template 'entity.rb.erb', File.join('app/api/houdini/v1/entities', "#{name.underscore}.rb") end end diff --git a/lib/generators/api/resource/resource_generator.rb b/lib/generators/api/resource/resource_generator.rb index 99aef6b1..a233320f 100644 --- a/lib/generators/api/resource/resource_generator.rb +++ b/lib/generators/api/resource/resource_generator.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails/generators' class Api::ResourceGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_to_resource - template 'resource.rb.erb', File.join("app/api/houdini/v1", "#{name.underscore}.rb") + template 'resource.rb.erb', File.join('app/api/houdini/v1', "#{name.underscore}.rb") end def copy_to_spec - template 'spec.rb.erb', File.join("spec/api/houdini/", "#{name.underscore}_spec.rb") + template 'spec.rb.erb', File.join('spec/api/houdini/', "#{name.underscore}_spec.rb") end def add_to_root_api - inject_into_file "app/api/houdini/v1/api.rb", "mount Houdini::V1::#{ name.camelcase} => \"/#{name.underscore}\"\n ", before:"# Additional mounts are added via generators above this line" + inject_into_file 'app/api/houdini/v1/api.rb', "mount Houdini::V1::#{name.camelcase} => \"/#{name.underscore}\"\n ", before: '# Additional mounts are added via generators above this line' end end diff --git a/lib/generators/api/validator/validator_generator.rb b/lib/generators/api/validator/validator_generator.rb index 7c57d094..23492492 100644 --- a/lib/generators/api/validator/validator_generator.rb +++ b/lib/generators/api/validator/validator_generator.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails/generators' class Api::ValidatorGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_to_validators - post_api_part = File.join("houdini/v1/validators", "#{name.underscore}.rb") - output_file = File.join("app/api", post_api_part ) + post_api_part = File.join('houdini/v1/validators', "#{name.underscore}.rb") + output_file = File.join('app/api', post_api_part) template 'validator.rb.erb', output_file end def add_to_root_validations - post_api_part = File.join("houdini/v1/validators", "#{name.underscore}") - append_to_file "app/api/houdini/v1/validations.rb", "\nrequire '#{post_api_part}'" + post_api_part = File.join('houdini/v1/validators', name.underscore.to_s) + append_to_file 'app/api/houdini/v1/validations.rb', "\nrequire '#{post_api_part}'" end end diff --git a/lib/generators/email_job/email_job_generator.rb b/lib/generators/email_job/email_job_generator.rb index 64de7f90..67138a48 100644 --- a/lib/generators/email_job/email_job_generator.rb +++ b/lib/generators/email_job/email_job_generator.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailJobGenerator < Rails::Generators::NamedBase - argument :attribs, :type => :array - source_root File.expand_path('../templates', __FILE__) + argument :attribs, type: :array + source_root File.expand_path('templates', __dir__) def copy_file_to_lib template 'email_job_template.erb', "lib/job_types/#{name.underscore}.rb" template 'email_job_spec_template.erb', "spec/lib/job_types/#{name.underscore}_spec.rb" diff --git a/lib/generators/libmodule/libmodule_generator.rb b/lib/generators/libmodule/libmodule_generator.rb index 0232b220..d82497e9 100644 --- a/lib/generators/libmodule/libmodule_generator.rb +++ b/lib/generators/libmodule/libmodule_generator.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class LibmoduleGenerator < Rails::Generators::NamedBase - argument :mod_type, :type => :string - source_root File.expand_path('../templates', __FILE__) + argument :mod_type, type: :string + source_root File.expand_path('templates', __dir__) def copy_file_to_lib template 'libmodule_template.erb', "lib/#{mod_type.underscore}/#{mod_type.underscore}_#{name.underscore}.rb" template 'libmodule_spec_template.erb', "spec/lib/#{mod_type.underscore}/#{mod_type.underscore}_#{name.underscore}_spec.rb" diff --git a/lib/generators/react/component/component_generator.rb b/lib/generators/react/component/component_generator.rb index 96b6b92e..fa2ae5a4 100644 --- a/lib/generators/react/component/component_generator.rb +++ b/lib/generators/react/component/component_generator.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class React::ComponentGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_file_to_component - template 'component.tsx.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.camelize}.tsx"])) - template 'component.spec.tsx.erb', File.join("javascripts/src/components", *(class_path + ["#{file_name.camelize}.spec.tsx"])) + template 'component.tsx.erb', File.join('javascripts/src/components', *(class_path + ["#{file_name.camelize}.tsx"])) + template 'component.spec.tsx.erb', File.join('javascripts/src/components', *(class_path + ["#{file_name.camelize}.spec.tsx"])) end end diff --git a/lib/generators/react/lib/lib_generator.rb b/lib/generators/react/lib/lib_generator.rb index 1f07f34e..f40bcb67 100644 --- a/lib/generators/react/lib/lib_generator.rb +++ b/lib/generators/react/lib/lib_generator.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + class React::LibGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_file_to_lib - template 'module.ts.erb', File.join("javascripts/src/lib/", *(class_path + ["#{file_name.underscore}.ts"])) - template 'module.spec.ts.erb', File.join("javascripts/src/lib/", *(class_path + ["#{file_name.underscore}.spec.ts"])) + template 'module.ts.erb', File.join('javascripts/src/lib/', *(class_path + ["#{file_name.underscore}.ts"])) + template 'module.spec.ts.erb', File.join('javascripts/src/lib/', *(class_path + ["#{file_name.underscore}.spec.ts"])) end end diff --git a/lib/generators/react/packroot/packroot_generator.rb b/lib/generators/react/packroot/packroot_generator.rb index a684165a..06354227 100644 --- a/lib/generators/react/packroot/packroot_generator.rb +++ b/lib/generators/react/packroot/packroot_generator.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module React class PackrootGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_file_to_app template 'page.tsx.erb', "javascripts/app/#{file_name.underscore}.tsx" generate 'react:component', "#{file_name.underscore}/#{file_name.camelize}" end end end - diff --git a/lib/generators/ts/declaration/declaration_generator.rb b/lib/generators/ts/declaration/declaration_generator.rb index 3c1a9e09..d7bbc609 100644 --- a/lib/generators/ts/declaration/declaration_generator.rb +++ b/lib/generators/ts/declaration/declaration_generator.rb @@ -1,7 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Ts::DeclarationGenerator < Rails::Generators::NamedBase - source_root File.expand_path('../templates', __FILE__) + source_root File.expand_path('templates', __dir__) def copy_template - template 'template.d.ts.erb', File.join("types", name, 'index.d.ts') + template 'template.d.ts.erb', File.join('types', name, 'index.d.ts') end end diff --git a/lib/geocode_model.rb b/lib/geocode_model.rb index cf802e54..2d4903c6 100644 --- a/lib/geocode_model.rb +++ b/lib/geocode_model.rb @@ -1,47 +1,46 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module GeocodeModel - def self.supporter(id) supp = Supporter.find_by_id(id) - if supp.address && supp.state_code && supp.city - with_reverse(supp) - end + with_reverse(supp) if supp.address && supp.state_code && supp.city end - # Just a wrapper around a model's geocode method for delaying with: - # GeocodeModel.delay.geocode(user) - def self.geocode(model) - begin - model.geocode - rescue Exception => e - puts e - end - model.save - model - end + # Just a wrapper around a model's geocode method for delaying with: + # GeocodeModel.delay.geocode(user) + def self.geocode(model) + begin + model.geocode + rescue Exception => e + puts e + end + model.save + model + end - def self.with_reverse(model) - begin - model.geocode - model.reverse_geocode - rescue Exception => e - puts e - end - model.save - model - end + def self.with_reverse(model) + begin + model.geocode + model.reverse_geocode + rescue Exception => e + puts e + end + model.save + model + end - # Geocode and get the timezone for a model - def self.with_timezone(model) - begin - geocode(model) - rescue Exception => e - puts e - end - return model unless model.latitude && model.longitude + # Geocode and get the timezone for a model + def self.with_timezone(model) + begin + geocode(model) + rescue Exception => e + puts e + end + return model unless model.latitude && model.longitude - model.timezone = NearestTimeZone.to(model.latitude, model.longitude) - model.save - model - end + model.timezone = NearestTimeZone.to(model.latitude, model.longitude) + model.save + model + end end diff --git a/lib/get_data.rb b/lib/get_data.rb index 295fc19f..71ab6957 100644 --- a/lib/get_data.rb +++ b/lib/get_data.rb @@ -1,33 +1,33 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module GetData + def self.chain(obj, *methods) + methods.each do |m| + if m.is_a?(Array) + params = m[1..-1] + m = m[0] + end - def self.chain(obj, *methods) - methods.each do |m| - if m.is_a?(Array) - params = m[1..-1] - m = m[0] - end - - if obj != nil && obj.respond_to?(m) - obj = obj.send(m, *params) - elsif obj.respond_to?(:has_key?) && obj.has_key?(m) - obj = obj[m] - else - return nil - end - end - return obj - end + if !obj.nil? && obj.respond_to?(m) + obj = obj.send(m, *params) + elsif obj.respond_to?(:has_key?) && obj.key?(m) + obj = obj[m] + else + return nil + end + end + obj + end def self.hash(h, *keys) - keys.each do |k| - if h.has_key?(k) - h = h[k] - else - return nil - end - end - return h + keys.each do |k| + if h.key?(k) + h = h[k] + else + return nil + end + end + h end end - diff --git a/lib/hash.rb b/lib/hash.rb index d12f3b70..8c00bc76 100644 --- a/lib/hash.rb +++ b/lib/hash.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Hash def keep_keys(*keys) - keys = keys.map{|k| k.to_s} - clone.delete_if{|k,v| !keys.include?(k.to_s)} + keys = keys.map(&:to_s) + clone.delete_if { |k, _v| !keys.include?(k.to_s) } end def keep_keys!(*keys) - keys = keys.map{|k| k.to_s} - delete_if{|k,v| !keys.include?(k.to_s)} + keys = keys.map(&:to_s) + delete_if { |k, _v| !keys.include?(k.to_s) } end end diff --git a/lib/health_report.rb b/lib/health_report.rb index 53e4130c..e93ca417 100644 --- a/lib/health_report.rb +++ b/lib/health_report.rb @@ -1,34 +1,35 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'format/csv' require 'format/currency' - module HealthReport # Send an email report about what has happend on the servers and database in the last 24hrs, and how things are running # Returns a hash of metrics data def self.query_data # Transaction metrics - charges = Qx.select("COUNT(charges.id), SUM(charges.amount), SUM(charges.fee) as fees") - .from("charges") - .where("created_at > $d", d: 24.hours.ago) - .and_where("charges.status != 'failed'") - .ex.last + charges = Qx.select('COUNT(charges.id), SUM(charges.amount), SUM(charges.fee) as fees') + .from('charges') + .where('created_at > $d', d: 24.hours.ago) + .and_where("charges.status != 'failed'") + .ex.last # Recurring donation metrics - rec_dons = Qx.select("COUNT(id), SUM(amount)") - .from("recurring_donations") - .where("active=TRUE") - .ex.last + rec_dons = Qx.select('COUNT(id), SUM(amount)') + .from('recurring_donations') + .where('active=TRUE') + .ex.last # Info about disabled nonprofit accounts due to ident verification - disabled_nps = Qx.select("nonprofits.id", "nonprofits.name", "nonprofits.stripe_account_id") - .from("nonprofits") - .where("verification_status != 'verified'") - .and_where("created_at > $d", d: 3.months.ago) - .ex(format: 'csv') + disabled_nps = Qx.select('nonprofits.id', 'nonprofits.name', 'nonprofits.stripe_account_id') + .from('nonprofits') + .where("verification_status != 'verified'") + .and_where('created_at > $d', d: 3.months.ago) + .ex(format: 'csv') - return { + { charges_count: charges['count'], charges_sum: charges['sum'], charges_fees: charges['fees'], @@ -39,10 +40,10 @@ module HealthReport end # Given a hash of data, formats it into a multi-line string - def self.format_data data + def self.format_data(data) disabled_nps = Format::Csv.from_array(data[:recently_disabled_nps]) - return %Q( + %( Transaction Metrics for the last 24hrs: Total count: #{data[:charges_count]} Total amount: $#{Format::Currency.cents_to_dollars(data[:charges_sum])} diff --git a/lib/htp.rb b/lib/htp.rb index 7385bbb0..ee28d9ee 100644 --- a/lib/htp.rb +++ b/lib/htp.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Hamster Table Print diff --git a/lib/image.rb b/lib/image.rb index 9e7a8e65..c25bcc72 100644 --- a/lib/image.rb +++ b/lib/image.rb @@ -1,19 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Image + AssetPath = 'https://dmnsmycmdpaix.cloudfront.net/uploads' - AssetPath = "https://dmnsmycmdpaix.cloudfront.net/uploads" + DefaultProfileUrl = Settings.default.image.profile + DefaultNonprofitUrl = Settings.default.image.nonprofit + DefaultCampaignUrl = Settings.default.image.campaign - DefaultProfileUrl = Settings.default.image.profile; - DefaultNonprofitUrl = Settings.default.image.nonprofit; - DefaultCampaignUrl = Settings.default.image.campaign; - - def self._url(resource_name, image_name, version='normal') - %Q( + def self._url(resource_name, image_name, version = 'normal') + %( concat(#{Qexpr.quote AssetPath} , '/', #{Qexpr.quote resource_name} , '/', #{Qexpr.quote image_name} , '/', #{resource_name + '.id'} , '/', #{Qexpr.quote version}, '_', #{resource_name + '.' + image_name}) - ) + ) end end diff --git a/lib/import/import_civicrm_payments.rb b/lib/import/import_civicrm_payments.rb index 71bac5ff..257c74e9 100644 --- a/lib/import/import_civicrm_payments.rb +++ b/lib/import/import_civicrm_payments.rb @@ -1,93 +1,81 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ImportCivicrmPayments - ## MINIMALLY TESTED!!! def self.import_from_csv(csv_body, nonprofit, field_of_supporter_id) - Qx.transaction do - CSV::Converters[:blank_to_nil] = lambda do |field| - field && field.empty? ? nil : field + Qx.transaction do + CSV::Converters[:blank_to_nil] = lambda do |field| + field && field.empty? ? nil : field + end + + csv = CSV.new(csv_body, headers: true, converters: [:blank_to_nil]) + contrib_records = csv.to_a.map(&:to_hash) + pay_imp = PaymentImport.create(nonprofit: nonprofit) + + supporter_id_custom_field = CustomFieldMaster.where('nonprofit_id = ? AND name = ?', nonprofit.id, field_of_supporter_id).first + + unless supporter_id_custom_field + raise ParamValidation::ValidationError.new("There is no custom field for nonprofit #{nonprofit.id} and field named #{field_of_supporter_id}", key: :field_of_supporter_id) + end + + supporters_with_fields = Supporter.includes(:custom_field_joins).where('supporters.nonprofit_id = ? AND custom_field_joins.custom_field_master_id = ?', nonprofit.id, supporter_id_custom_field.id) + questionable_records = [] + contrib_records.each do |r| + our_supporter = supporters_with_fields.where('custom_field_joins.value = ?', r[field_of_supporter_id].to_s).first + unless our_supporter + questionable_records.push(r) + next + end + + known_fields = ['Date Received', 'Total Amount'] + + notes = '' + r.except(known_fields).keys.each do |k| + notes += "#{k}: #{r[k]}\n" + end + + offsite = nil + if r['payment_instrument'] == 'Check' + offsite = { kind: 'check', check_number: r['Check Number'] } + end + + puts r['Date Received'] + date_received = nil + + Time.use_zone('Pacific Time (US & Canada)') do + date_received = Time.zone.parse(r['Date Received']) + puts date_received + end + + d = InsertDonation.offsite( + { + amount: Format::Currency.dollars_to_cents(r['Total Amount']), + nonprofit_id: nonprofit.id, + supporter_id: our_supporter.id, + comment: notes, + date: date_received.to_s, + offsite_payment: offsite + }.with_indifferent_access + ) + puts d + pay_imp.donations.push(Donation.find(d[:json]['donation']['id'])) + end + questionable_records end - - csv = CSV.new(csv_body, :headers => true, :converters => [ :blank_to_nil]) - contrib_records = csv.to_a.map {|row| row.to_hash } - pay_imp = PaymentImport.create(nonprofit: nonprofit) - - supporter_id_custom_field = CustomFieldMaster.where("nonprofit_id = ? AND name = ?", nonprofit.id, field_of_supporter_id).first - - unless supporter_id_custom_field - raise ParamValidation::ValidationError.new("There is no custom field for nonprofit #{nonprofit.id} and field named #{field_of_supporter_id}", {key: :field_of_supporter_id}) - end - - supporters_with_fields = Supporter.includes(:custom_field_joins).where("supporters.nonprofit_id = ? AND custom_field_joins.custom_field_master_id = ?", nonprofit.id, supporter_id_custom_field.id) - questionable_records = [] - contrib_records.each {|r| - - our_supporter = supporters_with_fields.where('custom_field_joins.value = ?', r[field_of_supporter_id].to_s).first - unless our_supporter - questionable_records.push(r) - next - end - - known_fields = ['Date Received', 'Total Amount', ] - - notes = "" - r.except(known_fields).keys.each{|k| - notes += "#{k}: #{r[k]}\n" - } - - - offsite = nil - if r['payment_instrument'] == 'Check' - offsite = {kind: 'check', check_number: r['Check Number']} - end - - puts r['Date Received'] - date_received = nil - - - Time.use_zone('Pacific Time (US & Canada)') do - date_received = Time.zone.parse(r['Date Received']) - puts date_received - end - - - - - d = InsertDonation.offsite( - { - amount: Format::Currency.dollars_to_cents(r['Total Amount']), - nonprofit_id: nonprofit.id, - supporter_id: our_supporter.id, - comment: notes, - date: date_received.to_s, - offsite_payment: offsite - }.with_indifferent_access - ) - puts d - pay_imp.donations.push(Donation.find(d[:json]['donation']['id'])) - } - questionable_records - end end def self.undo(import_id) Qx.transaction do - import = PaymentImport.find(import_id) - import.donations.each{|d| + import = PaymentImport.find(import_id) + import.donations.each do |d| + d.payments.each(&:destroy) + d.offsite_payment&.destroy - d.payments.each{|p| - p.destroy - } - if d.offsite_payment - d.offsite_payment.destroy + d.destroy end - - d.destroy - - } - - import.destroy + import.destroy end end -end \ No newline at end of file +end diff --git a/lib/include_asset.rb b/lib/include_asset.rb index cd10bdb3..924946c1 100644 --- a/lib/include_asset.rb +++ b/lib/include_asset.rb @@ -1,18 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module IncludeAsset - # These are custom asset include functions for use in views that cache-bust using the current git version def self.js(path) - %Q().html_safe + %().html_safe end def self.css(path) - %Q().html_safe + %().html_safe end -private - + private + def self.asset_version ENV['ASSET_VERSION'] end diff --git a/lib/insert/insert_activities.rb b/lib/insert/insert_activities.rb index 7fe6aa18..5bfc572d 100644 --- a/lib/insert/insert_activities.rb +++ b/lib/insert/insert_activities.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'active_support/core_ext' @@ -5,9 +7,8 @@ require 'format/currency' require 'format/date' module InsertActivities - def self.insert_cols - ["action_type", "public", "created_at", "updated_at", "supporter_id", "attachment_type", "attachment_id", "nonprofit_id", "date", "json_data", "kind"] + %w[action_type public created_at updated_at supporter_id attachment_type attachment_id nonprofit_id date json_data kind] end # These line up with the above columns @@ -26,188 +27,186 @@ module InsertActivities def self.for_recurring_donations(payment_ids) insert_recurring_donations_expr - .and_where("payments.id IN ($ids)", ids: payment_ids) + .and_where('payments.id IN ($ids)', ids: payment_ids) .execute end def self.insert_recurring_donations_expr Qx.insert_into(:activities, insert_cols) .select(defaults.concat([ - "payments.supporter_id", - "'Payment' AS attachment_type", - "payments.id AS attachment_id", - "payments.nonprofit_id", - "payments.date", - "json_build_object('gross_amount', payments.gross_amount, 'start_date', donations.created_at, 'designation', donations.designation, 'dedication', donations.dedication, 'interval', recurring_donations.interval, 'time_unit', recurring_donations.time_unit)", - "'RecurringDonation' AS kind" - ])) + 'payments.supporter_id', + "'Payment' AS attachment_type", + 'payments.id AS attachment_id', + 'payments.nonprofit_id', + 'payments.date', + "json_build_object('gross_amount', payments.gross_amount, 'start_date', donations.created_at, 'designation', donations.designation, 'dedication', donations.dedication, 'interval', recurring_donations.interval, 'time_unit', recurring_donations.time_unit)", + "'RecurringDonation' AS kind" + ])) .from(:payments) - .join(:donations, "donations.id=payments.donation_id") - .add_join(:recurring_donations, "recurring_donations.donation_id=donations.id") + .join(:donations, 'donations.id=payments.donation_id') + .add_join(:recurring_donations, 'recurring_donations.donation_id=donations.id') .where("payments.kind='RecurringDonation'") end def self.for_one_time_donations(payment_ids) insert_one_time_donations_expr - .and_where("payments.id IN ($ids)", ids: payment_ids) + .and_where('payments.id IN ($ids)', ids: payment_ids) .execute end def self.insert_one_time_donations_expr Qx.insert_into(:activities, insert_cols) .select(defaults.concat([ - "payments.supporter_id", - "'Payment' AS attachment_type", - "payments.id AS attachment_id", - "payments.nonprofit_id", - "payments.date", - "json_build_object('gross_amount', payments.gross_amount, 'designation', donations.designation, 'dedication', donations.dedication)", - "'Donation' AS kind" - ])) + 'payments.supporter_id', + "'Payment' AS attachment_type", + 'payments.id AS attachment_id', + 'payments.nonprofit_id', + 'payments.date', + "json_build_object('gross_amount', payments.gross_amount, 'designation', donations.designation, 'dedication', donations.dedication)", + "'Donation' AS kind" + ])) .from(:payments) - .join(:donations, "donations.id=payments.donation_id") + .join(:donations, 'donations.id=payments.donation_id') .where("payments.kind='Donation'") end - + def self.for_tickets(ticket_ids) insert_tickets_expr - .and_where("tickets.id IN ($ids)", ids: ticket_ids) + .and_where('tickets.id IN ($ids)', ids: ticket_ids) .execute end def self.insert_tickets_expr Qx.insert_into(:activities, insert_cols) - .select(defaults.concat([ - "tickets.supporter_id", - "'Ticket' AS attachment_type", - "tickets.id AS attachment_id", - "event.nonprofit_id", - "tickets.created_at AS date", - "json_build_object('gross_amount', coalesce(payment.gross_amount, 0), 'event_name', event.name, 'event_id', event.id, 'quantity', tickets.quantity)", - "'Ticket' AS kind" - ])) + .select(defaults.concat([ + 'tickets.supporter_id', + "'Ticket' AS attachment_type", + 'tickets.id AS attachment_id', + 'event.nonprofit_id', + 'tickets.created_at AS date', + "json_build_object('gross_amount', coalesce(payment.gross_amount, 0), 'event_name', event.name, 'event_id', event.id, 'quantity', tickets.quantity)", + "'Ticket' AS kind" + ])) .from(:tickets) - .join("payments AS payment", "payment.id=tickets.payment_id") - .add_join("events AS event", "event.id=tickets.event_id") + .join('payments AS payment', 'payment.id=tickets.payment_id') + .add_join('events AS event', 'event.id=tickets.event_id') end def self.for_refunds(payment_ids) insert_refunds_expr - .and_where("payments.id IN ($ids)", ids: payment_ids) + .and_where('payments.id IN ($ids)', ids: payment_ids) .execute end def self.insert_refunds_expr - Qx.insert_into(:activities, insert_cols.concat(["user_id"])) + Qx.insert_into(:activities, insert_cols.concat(['user_id'])) .select(defaults.concat([ - "payments.supporter_id", - "'Payment' AS attachment_type", - "payments.id AS attachment_id", - "payments.nonprofit_id", - "payments.date", - "json_build_object('gross_amount', payments.gross_amount, 'reason', refunds.reason, 'user_email', users.email)", - "'Refund' AS kind", - "users.id AS user_id" - ])) + 'payments.supporter_id', + "'Payment' AS attachment_type", + 'payments.id AS attachment_id', + 'payments.nonprofit_id', + 'payments.date', + "json_build_object('gross_amount', payments.gross_amount, 'reason', refunds.reason, 'user_email', users.email)", + "'Refund' AS kind", + 'users.id AS user_id' + ])) .from(:payments) - .join(:refunds, "refunds.payment_id=payments.id") - .left_join(:users, "refunds.user_id=users.id") + .join(:refunds, 'refunds.payment_id=payments.id') + .left_join(:users, 'refunds.user_id=users.id') .where("payments.kind='Refund'") end def self.for_disputes(payment_ids) insert_disputes_expr - .and_where("payments.id IN ($ids)", ids: payment_ids) + .and_where('payments.id IN ($ids)', ids: payment_ids) .execute end def self.insert_disputes_expr Qx.insert_into(:activities, insert_cols) .select(defaults.concat([ - "payments.supporter_id", - "'Payment' AS attachment_type", - "payments.id AS attachment_id", - "payments.nonprofit_id", - "payments.date", - "json_build_object('gross_amount', payments.gross_amount, 'reason', disputes.reason, 'original_kind', other_payment.kind, 'original_date', other_payment.date)", - "'Dispute' AS kind" - ])) + 'payments.supporter_id', + "'Payment' AS attachment_type", + 'payments.id AS attachment_id', + 'payments.nonprofit_id', + 'payments.date', + "json_build_object('gross_amount', payments.gross_amount, 'reason', disputes.reason, 'original_kind', other_payment.kind, 'original_date', other_payment.date)", + "'Dispute' AS kind" + ])) .from(:payments) - .join(:disputes, "disputes.payment_id=payments.id") - .add_join(:charges, "disputes.charge_id=charges.id") - .add_join("payments AS other_payment", "other_payment.id=charges.payment_id") + .join(:disputes, 'disputes.payment_id=payments.id') + .add_join(:charges, 'disputes.charge_id=charges.id') + .add_join('payments AS other_payment', 'other_payment.id=charges.payment_id') .where("payments.kind='Dispute'") end def self.for_supporter_emails(ids) insert_supporter_emails_expr - .and_where("supporter_emails.id IN ($ids)", ids: ids) + .and_where('supporter_emails.id IN ($ids)', ids: ids) .execute end def self.insert_supporter_emails_expr - Qx.insert_into(:activities, insert_cols.concat(["user_id"])) - .select(defaults.concat([ - "supporter_emails.supporter_id", - "'SupporterEmail' AS attachment_type", - "supporter_emails.id AS attachment_id", - "supporter_emails.nonprofit_id", - "supporter_emails.created_at AS date", - "json_build_object('gmail_thread_id', supporter_emails.gmail_thread_id, 'subject', supporter_emails.subject, 'from', supporter_emails.from)", - "'SupporterEmail' AS kind", - "users.id AS user_id" - ])) + Qx.insert_into(:activities, insert_cols.concat(['user_id'])) + .select(defaults.concat([ + 'supporter_emails.supporter_id', + "'SupporterEmail' AS attachment_type", + 'supporter_emails.id AS attachment_id', + 'supporter_emails.nonprofit_id', + 'supporter_emails.created_at AS date', + "json_build_object('gmail_thread_id', supporter_emails.gmail_thread_id, 'subject', supporter_emails.subject, 'from', supporter_emails.from)", + "'SupporterEmail' AS kind", + 'users.id AS user_id' + ])) .from(:supporter_emails) - .left_join(:users, "users.id=supporter_emails.user_id") + .left_join(:users, 'users.id=supporter_emails.user_id') end def self.for_supporter_notes(ids) insert_supporter_notes_expr - .and_where("supporter_notes.id IN ($ids)", ids: ids) + .and_where('supporter_notes.id IN ($ids)', ids: ids) .execute end def self.insert_supporter_notes_expr - Qx.insert_into(:activities, insert_cols.concat(["user_id"])) - .select(defaults.concat([ - "supporter_notes.supporter_id", - "'SupporterEmail' AS attachment_type", - "supporter_notes.id AS attachment_id", - "supporters.nonprofit_id", - "supporter_notes.created_at AS date", - "json_build_object('content', supporter_notes.content, 'user_email', users.email)", - "'SupporterNote' AS kind", - "users.id AS user_id" - ])) + Qx.insert_into(:activities, insert_cols.concat(['user_id'])) + .select(defaults.concat([ + 'supporter_notes.supporter_id', + "'SupporterEmail' AS attachment_type", + 'supporter_notes.id AS attachment_id', + 'supporters.nonprofit_id', + 'supporter_notes.created_at AS date', + "json_build_object('content', supporter_notes.content, 'user_email', users.email)", + "'SupporterNote' AS kind", + 'users.id AS user_id' + ])) .from(:supporter_notes) - .join("supporters", "supporters.id=supporter_notes.supporter_id") - .add_left_join(:users, "users.id=supporter_notes.user_id") + .join('supporters', 'supporters.id=supporter_notes.supporter_id') + .add_left_join(:users, 'users.id=supporter_notes.user_id') end def self.for_offsite_donations(payment_ids) insert_offsite_donations_expr - .and_where("payments.id IN ($ids)", ids: payment_ids) + .and_where('payments.id IN ($ids)', ids: payment_ids) .execute end def self.insert_offsite_donations_expr - Qx.insert_into(:activities, insert_cols.concat(["user_id"])) - .select(defaults.concat([ - "payments.supporter_id", - "'Payment' AS attachment_type", - "payments.id AS attachment_id", - "payments.nonprofit_id", - "payments.date", - "json_build_object('gross_amount', payments.gross_amount, 'designation', donations.designation, 'user_email', users.email)", - "'OffsitePayment' AS kind", - "users.id AS user_id" - ])) + Qx.insert_into(:activities, insert_cols.concat(['user_id'])) + .select(defaults.concat([ + 'payments.supporter_id', + "'Payment' AS attachment_type", + 'payments.id AS attachment_id', + 'payments.nonprofit_id', + 'payments.date', + "json_build_object('gross_amount', payments.gross_amount, 'designation', donations.designation, 'user_email', users.email)", + "'OffsitePayment' AS kind", + 'users.id AS user_id' + ])) .from(:payments) .where("payments.kind = 'OffsitePayment'") - .join(:offsite_payments, "offsite_payments.payment_id=payments.id") - .add_join(:donations, "payments.donation_id=donations.id") - .add_left_join(:users, "users.id=offsite_payments.user_id") + .join(:offsite_payments, 'offsite_payments.payment_id=payments.id') + .add_join(:donations, 'payments.donation_id=donations.id') + .add_left_join(:users, 'users.id=offsite_payments.user_id') end - - end diff --git a/lib/insert/insert_bank_account.rb b/lib/insert/insert_bank_account.rb index c96f1b70..d6e7e32d 100644 --- a/lib/insert/insert_bank_account.rb +++ b/lib/insert/insert_bank_account.rb @@ -1,67 +1,63 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertBankAccount - # @param [Nonprofit] nonprofit # # stripe_bank_account_token: data.stripe_resp.id, - #stripe_bank_account_id: data.stripe_resp.bank_account.id, - # name: data.stripe_resp.bank_account.bank_name + ' *' + data.stripe_resp.bank_account.last4, - # email: app.user.email + # stripe_bank_account_id: data.stripe_resp.bank_account.id, + # name: data.stripe_resp.bank_account.bank_name + ' *' + data.stripe_resp.bank_account.last4, + # email: app.user.email def self.with_stripe(nonprofit, user, params) - ParamValidation.new({nonprofit: nonprofit, user: user}, { - :nonprofit => { - :required => true, - :is_a => Nonprofit - }, - :user => { - :required => true, - :is_a => User - } - }) - ParamValidation.new(params|| {}, { - :stripe_bank_account_token => { - :required => true, - :not_blank => true - } - }) + ParamValidation.new({ nonprofit: nonprofit, user: user }, + nonprofit: { + required: true, + is_a: Nonprofit + }, + user: { + required: true, + is_a: User + }) + ParamValidation.new(params || {}, + stripe_bank_account_token: { + required: true, + not_blank: true + }) - unless (nonprofit.vetted) - raise ArgumentError.new "#{nonprofit.id} is not vetted." + unless nonprofit.vetted + raise ArgumentError, "#{nonprofit.id} is not vetted." end stripe_acct = Stripe::Account.retrieve(StripeAccount.find_or_create(nonprofit.id)) nonprofit.reload - #this shouldn't be possible but we'll check any who - if (nonprofit.stripe_account_id.blank?) - raise ArgumentError.new "#{nonprofit.id} does not have a valid stripe_account_id associated with it" + # this shouldn't be possible but we'll check any who + if nonprofit.stripe_account_id.blank? + raise ArgumentError, "#{nonprofit.id} does not have a valid stripe_account_id associated with it" end Qx.transaction do - begin - ba = stripe_acct.external_accounts.create(external_account: params[:stripe_bank_account_token]) - ba.default_for_currency = true - ba.save + ba = stripe_acct.external_accounts.create(external_account: params[:stripe_bank_account_token]) + ba.default_for_currency = true + ba.save - BankAccount.where('nonprofit_id = ?', nonprofit.id).update_all(deleted: true) + BankAccount.where('nonprofit_id = ?', nonprofit.id).update_all(deleted: true) - bank_account = BankAccount.create( - stripe_bank_account_id: ba.id, - stripe_bank_account_token: params[:stripe_bank_account_token], - confirmation_token: SecureRandom.uuid, - nonprofit: nonprofit, - name: params[:name] || "Bank #{SecureRandom.uuid}", - email: user.email, - pending_verification: true - ) + bank_account = BankAccount.create( + stripe_bank_account_id: ba.id, + stripe_bank_account_token: params[:stripe_bank_account_token], + confirmation_token: SecureRandom.uuid, + nonprofit: nonprofit, + name: params[:name] || "Bank #{SecureRandom.uuid}", + email: user.email, + pending_verification: true + ) - NonprofitMailer.delay.new_bank_account_notification(bank_account) - return bank_account - rescue Stripe::StripeError => error - params[:failure_message] = "Failed to connect the bank account: #{error.inspect}" - raise ArgumentError.new("Failed to connect the bank account: #{error.inspect}") - end + NonprofitMailer.delay.new_bank_account_notification(bank_account) + return bank_account + rescue Stripe::StripeError => error + params[:failure_message] = "Failed to connect the bank account: #{error.inspect}" + raise ArgumentError, "Failed to connect the bank account: #{error.inspect}" end - end -end \ No newline at end of file +end diff --git a/lib/insert/insert_billing_subscriptions.rb b/lib/insert/insert_billing_subscriptions.rb index 48bfecc0..be663533 100644 --- a/lib/insert/insert_billing_subscriptions.rb +++ b/lib/insert/insert_billing_subscriptions.rb @@ -1,23 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'delayed_job_helper' require 'active_support/core_ext' - module InsertBillingSubscriptions - def self.trial(np_id, stripe_plan_id) - begin - nonprofit = Nonprofit.includes(:billing_subscription).find(np_id) - billing_plan = BillingPlan.where('stripe_plan_id = ?', stripe_plan_id).last - sub = nonprofit.create_billing_subscription(billing_plan: billing_plan, status: 'trialing') - n = 10 - DelayedJobHelper.enqueue_job(self, :check_trial, [sub['id']], {run_at: n.days.from_now}) - return {json: sub} - rescue ActiveRecord::RecordNotFound => e - return {json: { error: e }, status: :unprocessable_entity} - end - + nonprofit = Nonprofit.includes(:billing_subscription).find(np_id) + billing_plan = BillingPlan.where('stripe_plan_id = ?', stripe_plan_id).last + sub = nonprofit.create_billing_subscription(billing_plan: billing_plan, status: 'trialing') + n = 10 + DelayedJobHelper.enqueue_job(self, :check_trial, [sub['id']], run_at: n.days.from_now) + { json: sub } + rescue ActiveRecord::RecordNotFound => e + { json: { error: e }, status: :unprocessable_entity } end def self.check_trial(bs_id) @@ -26,9 +23,8 @@ module InsertBillingSubscriptions Qx.update(:billing_subscriptions) .set(status: 'inactive') .timestamps - .where("id = $id", id: bs_id) + .where('id = $id', id: bs_id) .execute end end - end diff --git a/lib/insert/insert_card.rb b/lib/insert/insert_card.rb index dfb7709e..ffd6504b 100644 --- a/lib/insert/insert_card.rb +++ b/lib/insert/insert_card.rb @@ -1,8 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'hash' module InsertCard - - # Create a new card # If a stripe_customer_id is present, then update that customer's primary source; otherwise create a new customer # @param [ActiveSupport::HashWithIndifferentAccess] card_data card data @@ -17,60 +17,55 @@ module InsertCard # @param [String] stripe_account_id not clear what this should do. # @param [Integer] event_id id for events with when you want it associated with an event - # @param [User] current_user the user making the request. Used for validating that the current_user can make a long term token request - def self.with_stripe(card_data, stripe_account_id=nil, event_id=nil, current_user = nil) - + # @param [User] current_user the user making the request. Used for validating that the current_user can make a long term token request + def self.with_stripe(card_data, _stripe_account_id = nil, event_id = nil, current_user = nil) begin - ParamValidation.new(card_data.merge({event_id: event_id}), { - holder_type: {required: true, included_in: ['Nonprofit', 'Supporter']}, - holder_id: {required: true}, - stripe_card_id: {not_blank: true, required: true}, - stripe_card_token: {not_blank: true, required: true}, - name: {not_blank: true, required: true}, - event_id: {is_reference: true} - }) + ParamValidation.new(card_data.merge(event_id: event_id), + holder_type: { required: true, included_in: %w[Nonprofit Supporter] }, + holder_id: { required: true }, + stripe_card_id: { not_blank: true, required: true }, + stripe_card_token: { not_blank: true, required: true }, + name: { not_blank: true, required: true }, + event_id: { is_reference: true }) rescue ParamValidation::ValidationError => e - return {json: {error: "Validation error\n #{e.message}", errors: e.data}, status: :unprocessable_entity} + return { json: { error: "Validation error\n #{e.message}", errors: e.data }, status: :unprocessable_entity } end - - # validate that the user is with the correct nonprofit - card_data = card_data.keep_keys(:holder_type, :holder_id, :stripe_card_id, :stripe_card_token, :name ) - holder_types = {'Nonprofit' => :nonprofit, 'Supporter' => :supporter} + card_data = card_data.keep_keys(:holder_type, :holder_id, :stripe_card_id, :stripe_card_token, :name) + holder_types = { 'Nonprofit' => :nonprofit, 'Supporter' => :supporter } holder_type = holder_types[card_data[:holder_type]] holder = nil begin if holder_type == :nonprofit - holder = Nonprofit.select("id, email").includes(:cards).find(card_data[:holder_id]) + holder = Nonprofit.select('id, email').includes(:cards).find(card_data[:holder_id]) elsif holder_type == :supporter - holder = Supporter.select("id, email, nonprofit_id").includes(:cards, :nonprofit).find(card_data[:holder_id]) + holder = Supporter.select('id, email, nonprofit_id').includes(:cards, :nonprofit).find(card_data[:holder_id]) end rescue ActiveRecord::RecordNotFound - return {json: {error: "Sorry, you need to provide a nonprofit or supporter"}, status: :unprocessable_entity} + return { json: { error: 'Sorry, you need to provide a nonprofit or supporter' }, status: :unprocessable_entity } end begin if holder_type == :supporter && event_id event = Event.where('id = ?', event_id).first unless event - raise ParamValidation::ValidationError.new("#{event_id} is not a valid event", {key: :event_id}) + raise ParamValidation::ValidationError.new("#{event_id} is not a valid event", key: :event_id) end - if (holder.nonprofit != event.nonprofit ) - raise ParamValidation::ValidationError.new("Event #{event_id} is not for the same nonprofit as supporter #{holder.id}", {key: :event_id}) + if holder.nonprofit != event.nonprofit + raise ParamValidation::ValidationError.new("Event #{event_id} is not for the same nonprofit as supporter #{holder.id}", key: :event_id) end unless QueryRoles.is_authorized_for_nonprofit?(current_user.id, holder.nonprofit.id) - raise AuthenticationError.new + raise AuthenticationError end end rescue AuthenticationError - return {json: {error: "You're not authorized to perform that action"}, status: :unauthorized} - rescue => e - return {json: {error: "Oops! There was an error: #{e.message}"}, status: :unprocessable_entity} - + return { json: { error: "You're not authorized to perform that action" }, status: :unauthorized } + rescue StandardError => e + return { json: { error: "Oops! There was an error: #{e.message}" }, status: :unprocessable_entity } end stripe_account_hash = {} # stripe_account_id ? {stripe_account: stripe_account_id} : {} begin @@ -84,43 +79,39 @@ module InsertCard card_data[:stripe_customer_id] = stripe_customer.id rescue Stripe::CardError => e - return {json: {error: "Oops! #{e.json_body[:error][:message]}"}, status: :unprocessable_entity} + return { json: { error: "Oops! #{e.json_body[:error][:message]}" }, status: :unprocessable_entity } rescue Stripe::StripeError => e - return {json: {error: "Oops! There was an error processing your payment, and it did not complete. Please try again in a moment. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error processing your payment, and it did not complete. Please try again in a moment. Error: #{e}" }, status: :unprocessable_entity } end card = nil source_token = nil begin - Card.transaction { - - if (holder_type == :nonprofit) + Card.transaction do + if holder_type == :nonprofit # @type [Nonprofit] holder card = holder.create_active_card(card_data) - elsif (holder_type == :supporter) + elsif holder_type == :supporter # @type [Supporter] holder card = holder.cards.create(card_data) params = {} - if event - params[:event] = event - end + params[:event] = event if event source_token = InsertSourceToken.create_record(card, params).token end card.save! - } + end rescue ActiveRecord::ActiveRecordError => e - return {json: {error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}" }, status: :unprocessable_entity } rescue e - return {json: {error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}" }, status: :unprocessable_entity } rescue e - return {json: {error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error saving your card, and it did not complete. Please try again in a moment. Error: #{e}" }, status: :unprocessable_entity } end - return { status: :ok, json: card.attributes.with_indifferent_access.merge(token: source_token) } - end + { status: :ok, json: card.attributes.with_indifferent_access.merge(token: source_token) } +end def self.customer_data(holder, card_data) { email: holder['email'], metadata: { cardholders_name: card_data[:cardholders_name], holder_id: card_data[:holder_id], holder_type: card_data[:holder_type] } } end - end diff --git a/lib/insert/insert_charge.rb b/lib/insert/insert_charge.rb index 15433c83..c8390ee4 100644 --- a/lib/insert/insert_charge.rb +++ b/lib/insert/insert_charge.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' @@ -6,161 +8,155 @@ require 'stripe' require 'get_data' require 'active_support/core_ext' require 'query/billing_plans' -require 'stripe_account' unless !Settings.payment_provider.stripe_connect +require 'stripe_account' if Settings.payment_provider.stripe_connect module InsertCharge - # In data, pass in: amount, nonprofit_id, supporter_id, card_id, statement # Optionally pass in :metadata for stripe and donation_id to connect to donation? # @raise [ParamValidation::ValidationError] parameter validation occurred # @raise [Stripe::StripeError] the stripe account couldn't be accessed or created def self.with_stripe(data) + ParamValidation.new(data || {}, + amount: { + required: true, + is_integer: true, + min: 0 + }, + nonprofit_id: { + required: true, + is_integer: true + }, + supporter_id: { + required: true, + is_integer: true + }, + card_id: { + required: true, + is_integer: true + }, + statement: { + required: true, + not_blank: true + }) + + np = Nonprofit.where('id = ?', data[:nonprofit_id]).first + + unless np + raise ParamValidation::ValidationError.new("#{data[:nonprofit_id]} is not a valid Nonprofit", key: :nonprofit_id) + end + + supporter = Supporter.where('id = ?', data[:supporter_id]).first + + unless supporter + raise ParamValidation::ValidationError.new("#{data[:supporter_id]} is not a valid Supporter", key: :supporter_id) + end + + card = Card.where('id = ?', data[:card_id]).first + + unless card + raise ParamValidation::ValidationError.new("#{data[:card_id]} is not a valid card", key: :card_id) + end + + unless np == supporter.nonprofit + raise ParamValidation::ValidationError.new("#{data[:supporter_id]} does not belong to this nonprofit #{np.id}", key: :supporter_id) + end + + unless card.holder == supporter + if data[:old_donation] + # these are not new donations so we let them fly (for now) + else + raise ParamValidation::ValidationError.new("#{data[:card_id]} does not belong to this supporter #{supporter.id}", key: :card_id) + end + end + + result = {} + # Catch errors thrown by the stripe gem so we can respond with a 422 with an error message rather than 500 begin - ParamValidation.new(data || {}, { - :amount => { - :required => true, - :is_integer => true, - :min => 0 - }, - :nonprofit_id => { - :required => true, - :is_integer => true - }, - :supporter_id => { - :required => true, - :is_integer => true - }, - :card_id => { - :required => true, - :is_integer => true - }, - :statement => { - :required => true, - :not_blank => true - } - }) + stripe_customer_id = card.stripe_customer_id + stripe_account_id = StripeAccount.find_or_create(data[:nonprofit_id]) + rescue StandardError => e + raise e + end + nonprofit_currency = Qx.select(:currency).from(:nonprofits).where('id=$id', id: data[:nonprofit_id]).execute.first['currency'] - np = Nonprofit.where('id = ?', data[:nonprofit_id]).first + stripe_charge_data = { + customer: stripe_customer_id, + amount: data[:amount], + currency: nonprofit_currency, + description: data[:statement], + statement_descriptor: data[:statement][0..21].gsub(/[<>"']/, ''), + metadata: data[:metadata] + } - unless np - raise ParamValidation::ValidationError.new("#{data[:nonprofit_id]} is not a valid Nonprofit", {:key => :nonprofit_id}) - end - - supporter = Supporter.where('id = ?', data[:supporter_id]).first - - unless supporter - raise ParamValidation::ValidationError.new("#{data[:supporter_id]} is not a valid Supporter", {:key => :supporter_id}) - end - - card = Card.where('id = ?', data[:card_id]).first - - unless card - raise ParamValidation::ValidationError.new("#{data[:card_id]} is not a valid card", {:key => :card_id}) - end - - unless np == supporter.nonprofit - raise ParamValidation::ValidationError.new("#{data[:supporter_id]} does not belong to this nonprofit #{np.id}", {:key => :supporter_id}) - end - - unless card.holder == supporter - if (data[:old_donation]) - #these are not new donations so we let them fly (for now) - else - raise ParamValidation::ValidationError.new("#{data[:card_id]} does not belong to this supporter #{supporter.id}", {:key => :card_id}) - end - end - - result = {} - # Catch errors thrown by the stripe gem so we can respond with a 422 with an error message rather than 500 - begin - stripe_customer_id = card.stripe_customer_id - stripe_account_id = StripeAccount.find_or_create(data[:nonprofit_id]) - rescue => e - raise e - end - nonprofit_currency = Qx.select(:currency).from(:nonprofits).where("id=$id", id: data[:nonprofit_id]).execute.first['currency'] - - stripe_charge_data = { - customer: stripe_customer_id, - amount: data[:amount], - currency: nonprofit_currency, - description: data[:statement], - statement_descriptor: data[:statement][0..21].gsub(/[<>"']/,''), - metadata: data[:metadata] - } - - if Settings.payment_provider.stripe_connect - stripe_account_id = StripeAccount.find_or_create(data[:nonprofit_id]) - # Get the percentage fee on the nonprofit's billing plan - platform_fee = BillingPlans.get_percentage_fee(data[:nonprofit_id]) - fee = CalculateFees.for_single_amount(data[:amount], platform_fee) - stripe_charge_data[:application_fee]= fee + if Settings.payment_provider.stripe_connect + stripe_account_id = StripeAccount.find_or_create(data[:nonprofit_id]) + # Get the percentage fee on the nonprofit's billing plan + platform_fee = BillingPlans.get_percentage_fee(data[:nonprofit_id]) + fee = CalculateFees.for_single_amount(data[:amount], platform_fee) + stripe_charge_data[:application_fee] = fee # For backwards compatibility, see if the customer exists in the primary or the connected account # If it's a legacy customer, charge to the primary account and transfer with .destination # Otherwise, charge directly to the connected account - begin - stripe_cust = Stripe::Customer.retrieve(stripe_customer_id) - params = [stripe_charge_data.merge(destination: stripe_account_id), {}] - rescue - params = [stripe_charge_data, {stripe_account: stripe_account_id}] - end - else - fee=0 - stripe_charge_data[:source]=card['stripe_card_id'] - params = [stripe_charge_data, {}] - end - begin - stripe_charge = Stripe::Charge.create(*params) - rescue Stripe::CardError => e - failure_message = "There was an error with your card: #{e.json_body[:error][:message]}" - rescue Stripe::StripeError => e - failure_message = "We're sorry, but something went wrong. We've been notified about this issue." + stripe_cust = Stripe::Customer.retrieve(stripe_customer_id) + params = [stripe_charge_data.merge(destination: stripe_account_id), {}] + rescue StandardError + params = [stripe_charge_data, { stripe_account: stripe_account_id }] end + else + fee = 0 + stripe_charge_data[:source] = card['stripe_card_id'] + params = [stripe_charge_data, {}] + end + begin + stripe_charge = Stripe::Charge.create(*params) + rescue Stripe::CardError => e + failure_message = "There was an error with your card: #{e.json_body[:error][:message]}" + rescue Stripe::StripeError => e + failure_message = "We're sorry, but something went wrong. We've been notified about this issue." + end - charge = Charge.new + charge = Charge.new - charge.amount = data[:amount] - charge.fee = fee + charge.amount = data[:amount] + charge.fee = fee - charge.stripe_charge_id = GetData.chain(stripe_charge, :id) - charge.failure_message = failure_message - charge.status = GetData.chain(stripe_charge, :paid) ? 'pending' : 'failed' - charge.card = card - charge.donation = Donation.where('id = ?', data[:donation_id]).first - charge.supporter = supporter - charge.nonprofit = np + charge.stripe_charge_id = GetData.chain(stripe_charge, :id) + charge.failure_message = failure_message + charge.status = GetData.chain(stripe_charge, :paid) ? 'pending' : 'failed' + charge.card = card + charge.donation = Donation.where('id = ?', data[:donation_id]).first + charge.supporter = supporter + charge.nonprofit = np + charge.save! + result['charge'] = charge + + if stripe_charge && stripe_charge.status != 'failed' + payment = Payment.new + payment.gross_amount = data[:amount] + payment.fee_total = -fee + payment.net_amount = data[:amount] - fee + payment.towards = data[:towards] + payment.kind = data[:kind] + payment.donation = Donation.where('id = ?', data[:donation_id]).first + payment.nonprofit = np + payment.supporter = supporter + payment.refund_total = 0 + payment.date = data[:date] || result['charge'].created_at + payment.save! + + result['payment'] = payment + + charge.payment = payment charge.save! result['charge'] = charge - - if stripe_charge && stripe_charge.status != 'failed' - payment = Payment.new - payment.gross_amount = data[:amount] - payment.fee_total = -fee - payment.net_amount = data[:amount] - fee - payment.towards = data[:towards] - payment.kind = data[:kind] - payment.donation = Donation.where('id = ?', data[:donation_id]).first - payment.nonprofit = np - payment.supporter = supporter - payment.refund_total = 0 - payment.date = data[:date] || result['charge'].created_at - payment.save! - - - result['payment'] = payment - - charge.payment = payment - charge.save! - result['charge'] = charge - end - - return result - rescue => e - raise e end + + result + rescue StandardError => e + raise e end def self.with_sepa(data) @@ -171,7 +167,7 @@ module InsertCharge # TODO fee = 0 - #todo charge should be changed to SEPA charge + # TODO: charge should be changed to SEPA charge c = Charge.new c.direct_debit_detail = entities[:direct_debit_detail_id] @@ -186,9 +182,9 @@ module InsertCharge p = Payment.new - p.gross_amount= data[:amount] - p.fee_total= -fee - p.net_amount= data[:amount] - fee + p.gross_amount = data[:amount] + p.fee_total = -fee + p.net_amount = data[:amount] - fee p.towards = data[:towards] p.kind = data[:kind] p.nonprofit = entities[:nonprofit_id] diff --git a/lib/insert/insert_custom_field_joins.rb b/lib/insert/insert_custom_field_joins.rb index f25a38cd..d49d2b4c 100644 --- a/lib/insert/insert_custom_field_joins.rb +++ b/lib/insert/insert_custom_field_joins.rb @@ -1,131 +1,124 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'delayed_job_helper' require 'qx' require 'update/update_custom_field_joins' module InsertCustomFieldJoins - # Bulk insert many field joins into many supporters # for every field name, find or create it for the nonprofit # field_data should be an array of arrays liks [['Company', 'Pixar'], # ['Shirt-size', 'Small']] def self.find_or_create(np_id, supporter_ids, field_data) - ParamValidation.new({np_id: np_id, supporter_ids: supporter_ids, field_data: field_data}, - { - :np_id => { - :required => true, - :is_integer=> true - }, - :supporter_ids => { - :required => true, - :is_array => true, - :min_length => 1 - }, - :field_data => { - :required => true, - :is_array => true, - :min_length => 1 - } - }) + ParamValidation.new({ np_id: np_id, supporter_ids: supporter_ids, field_data: field_data }, + np_id: { + required: true, + is_integer: true + }, + supporter_ids: { + required: true, + is_array: true, + min_length: 1 + }, + field_data: { + required: true, + is_array: true, + min_length: 1 + }) - #make sure the np exists - np = Nonprofit.where("id = ? ", np_id).first + # make sure the np exists + np = Nonprofit.where('id = ? ', np_id).first unless np - raise ParamValidation::ValidationError.new("#{np_id} is not a valid non-profit", {:key => :np_id}) + raise ParamValidation::ValidationError.new("#{np_id} is not a valid non-profit", key: :np_id) end - #make sure the supporters_ids exist - supporter_ids.each {|id| + # make sure the supporters_ids exist + supporter_ids.each do |id| unless np.supporters.where('id = ?', id).exists? - raise ParamValidation::ValidationError.new("#{id} is not a valid supporter for nonprofit #{np_id}", {:key => :supporter_ids}) + raise ParamValidation::ValidationError.new("#{id} is not a valid supporter for nonprofit #{np_id}", key: :supporter_ids) end - } + end Qx.transaction do # get the custom_field_master_id for each field_data name - cfm_id_to_value = field_data.map { |name, value| - cfm = CustomFieldMaster.where("nonprofit_id = ? and name = ?", np_id, name).first - unless cfm - cfm = CustomFieldMaster.create!(:nonprofit => np, :name => name) - end - {:custom_field_master_id => cfm.id, :value => value} - } + cfm_id_to_value = field_data.map do |name, value| + cfm = CustomFieldMaster.where('nonprofit_id = ? and name = ?', np_id, name).first + cfm ||= CustomFieldMaster.create!(nonprofit: np, name: name) + { custom_field_master_id: cfm.id, value: value } + end return in_bulk(np_id, supporter_ids, cfm_id_to_value) end end - # Validation: *np_id is valid, corresponds to real nonprofit # # # @param [Integer] np_id nonprofit_id whose custom_fields this applies to # @param [Array] supporter_ids the supporter ids in which the custom fields should be modified # @param [Array>] field_data the fields you'd like to modify. Each item is a hash with following keys: - # * custom_field_master_id [Integer] for the key corresponding to custom_field_master_id - # * value [Object] the expected value of the field. If this key is an empty string, we remove the custom_field + # * custom_field_master_id [Integer] for the key corresponding to custom_field_master_id + # * value [Object] the expected value of the field. If this key is an empty string, we remove the custom_field def self.in_bulk(np_id, supporter_ids, field_data) begin ParamValidation.new({ - np_id: np_id, - supporter_ids: supporter_ids, - field_data: field_data - }, { - np_id: {required: true, is_integer: true}, - supporter_ids: {required:true, is_array: true}, - field_data: { required: true, is_array: true} - # array_of_hashes: { - # selected: {required: true}, tag_master_id: {required: true, is_integer: true} - # } - - }) + np_id: np_id, + supporter_ids: supporter_ids, + field_data: field_data + }, + np_id: { required: true, is_integer: true }, + supporter_ids: { required: true, is_array: true }, + field_data: { required: true, is_array: true }) + # array_of_hashes: { + # selected: {required: true}, tag_master_id: {required: true, is_integer: true} + # } rescue ParamValidation::ValidationError => e - return {json: {error: "Validation error\n #{e.message}", errors: e.data}, status: :unprocessable_entity} + return { json: { error: "Validation error\n #{e.message}", errors: e.data }, status: :unprocessable_entity } end begin - return {json: {error: "Nonprofit #{np_id} is not valid"}, status: :unprocessable_entity} unless Nonprofit.exists?(np_id) + return { json: { error: "Nonprofit #{np_id} is not valid" }, status: :unprocessable_entity } unless Nonprofit.exists?(np_id) # verify that the supporters belong to the nonprofit supporter_ids = Supporter.where('nonprofit_id = ? and id IN (?)', np_id, supporter_ids).pluck(:id) unless supporter_ids.any? - return {json: {inserted_count: 0, removed_count: 0}, status: :ok} + return { json: { inserted_count: 0, removed_count: 0 }, status: :ok } end # filtering the tag_data to this nonprofit - valid_ids = CustomFieldMaster.where('nonprofit_id = ? and id IN (?)', np_id, field_data.map {|fd| fd[:custom_field_master_id] }).pluck(:id).to_a - filtered_field_data = field_data.select {|i| valid_ids.include? i[:custom_field_master_id ].to_i} + valid_ids = CustomFieldMaster.where('nonprofit_id = ? and id IN (?)', np_id, field_data.map { |fd| fd[:custom_field_master_id] }).pluck(:id).to_a + filtered_field_data = field_data.select { |i| valid_ids.include? i[:custom_field_master_id].to_i } # first, delete the items which should be removed - to_insert, to_remove = filtered_field_data.partition{|t| + to_insert, to_remove = filtered_field_data.partition do |t| !t[:value].blank? - } + end deleted = [] if to_remove.any? deleted = Qx.delete_from(:custom_field_joins) - .where("supporter_id IN ($ids)", ids: supporter_ids) - .and_where("custom_field_master_id in ($fields)", fields: to_remove.map{|t| t[:custom_field_master_id]}) - .returning('*') - .execute + .where('supporter_id IN ($ids)', ids: supporter_ids) + .and_where('custom_field_master_id in ($fields)', fields: to_remove.map { |t| t[:custom_field_master_id] }) + .returning('*') + .execute end - # next add only the selected tag_joins if to_insert.any? - insert_data = supporter_ids.map{|id| to_insert.map{|cfm| {supporter_id: id, custom_field_master_id: cfm[:custom_field_master_id], value: cfm[:value]}}}.flatten + insert_data = supporter_ids.map { |id| to_insert.map { |cfm| { supporter_id: id, custom_field_master_id: cfm[:custom_field_master_id], value: cfm[:value] } } }.flatten cfj = Qx.insert_into(:custom_field_joins) - .values(insert_data) - .timestamps - .on_conflict() - .conflict_columns(:supporter_id, :custom_field_master_id).upsert(:custom_field_join_supporter_unique_idx) - .returning('*') - .execute + .values(insert_data) + .timestamps + .on_conflict + .conflict_columns(:supporter_id, :custom_field_master_id).upsert(:custom_field_join_supporter_unique_idx) + .returning('*') + .execute else cfj = [] end rescue ActiveRecord::ActiveRecordError => e - return {json: {error: "A DB error occurred. Please contact support. \n #{e.message}"}, status: :unprocessable_entity} + return { json: { error: "A DB error occurred. Please contact support. \n #{e.message}" }, status: :unprocessable_entity } end # Create an activity for the modified tags for every supporter @@ -134,10 +127,8 @@ module InsertCustomFieldJoins # activities = Psql.execute( Qexpr.new.insert(:activities, activity_data) ) # Sync mailchimp lists, if present - #Mailchimp.delay.sync_supporters_to_list_from_tag_joins(np_id, supporter_ids, tag_data) - - return {json: {inserted_count: cfj.count, removed_count: deleted.count }, status: :ok} + # Mailchimp.delay.sync_supporters_to_list_from_tag_joins(np_id, supporter_ids, tag_data) + { json: { inserted_count: cfj.count, removed_count: deleted.count }, status: :ok } end - end diff --git a/lib/insert/insert_direct_debit_detail.rb b/lib/insert/insert_direct_debit_detail.rb index cf3219a4..d4cc73d6 100644 --- a/lib/insert/insert_direct_debit_detail.rb +++ b/lib/insert/insert_direct_debit_detail.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertDirectDebitDetail def self.execute(params) @@ -5,18 +7,18 @@ module InsertDirectDebitDetail direct_debit_detail = {} begin - DirectDebitDetail.transaction { + DirectDebitDetail.transaction do direct_debit_detail = DirectDebitDetail.create( bic: params[:sepa_params][:bic], iban: params[:sepa_params][:iban], account_holder_name: params[:sepa_params][:name], holder: supporter ) - } + end rescue ActiveRecord::ActiveRecordError => e - return {json: {error: "Oops! There was an error saving your direct debit details, and it did not complete. Please try again in a moment. Error: #{e}"}, status: :unprocessable_entity} + return { json: { error: "Oops! There was an error saving your direct debit details, and it did not complete. Please try again in a moment. Error: #{e}" }, status: :unprocessable_entity } end - return { status: :ok, json: direct_debit_detail } + { status: :ok, json: direct_debit_detail } end end diff --git a/lib/insert/insert_disputes.rb b/lib/insert/insert_disputes.rb index 41222f80..c6cd7e53 100644 --- a/lib/insert/insert_disputes.rb +++ b/lib/insert/insert_disputes.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' @@ -5,42 +7,41 @@ require 'calculate/calculate_fees' require 'active_support/core_ext' module InsertDisputes - # A new dispute takes a charge id and dispute id and creates: # A dispute row with the charge gross amount and the dispute id # A payment row negative gross and net, just like a refund, but with kind "Dispute" def self.create_record(stripe_charge_id, stripe_dispute_id) # Find the existing charge - ch = Qx.select("*").from("charges").where("stripe_charge_id=$id", id: stripe_charge_id).ex.first - raise ArgumentError.new("Charge not found") if ch.nil? + ch = Qx.select('*').from('charges').where('stripe_charge_id=$id', id: stripe_charge_id).ex.first + raise ArgumentError, 'Charge not found' if ch.nil? result = {} now = Time.current result[:payment] = Psql.execute( Qexpr.new.insert(:payments, [{ - gross_amount: -ch['amount'], - fee_total: 0, - net_amount: -ch['amount'], - kind: 'Dispute', - refund_total: 0, - nonprofit_id: ch['nonprofit_id'], - supporter_id: ch['supporter_id'], - donation_id: ch['donation_id'], - date: now - }]).returning('*') + gross_amount: -ch['amount'], + fee_total: 0, + net_amount: -ch['amount'], + kind: 'Dispute', + refund_total: 0, + nonprofit_id: ch['nonprofit_id'], + supporter_id: ch['supporter_id'], + donation_id: ch['donation_id'], + date: now + }]).returning('*') ).first # Create a dispute record result[:dispute] = Psql.execute( Qexpr.new.insert(:disputes, [{ - gross_amount: ch['amount'], - status: :needs_response, - charge_id: ch['id'], - reason: :unrecognized, - payment_id: result[:payment]['id'], - stripe_dispute_id: stripe_dispute_id - }]).returning('*') + gross_amount: ch['amount'], + status: :needs_response, + charge_id: ch['id'], + reason: :unrecognized, + payment_id: result[:payment]['id'], + stripe_dispute_id: stripe_dispute_id + }]).returning('*') ).first # Prevent refunds from being able to happen on the payment @@ -49,8 +50,6 @@ module InsertDisputes # Insert an activity record InsertActivities.for_disputes([result[:payment]['id']]) - return result + result end - end - diff --git a/lib/insert/insert_donation.rb b/lib/insert/insert_donation.rb index 86875837..74a4d8c0 100644 --- a/lib/insert/insert_donation.rb +++ b/lib/insert/insert_donation.rb @@ -1,25 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertDonation - - # Make a one-time donation (call InsertRecurringDonation.with_stripe to create a recurring donation) # In data, pass in: # amount, card_id, nonprofit_id, supporter_id # designation, dedication # recurring_donation if is recurring - def self.with_stripe(data, current_user=nil) + def self.with_stripe(data, current_user = nil) data = data.with_indifferent_access ParamValidation.new(data, common_param_validations - .merge(token: {required: true, format: UUID::Regex})) + .merge(token: { required: true, format: UUID::Regex })) source_token = QuerySourceToken.get_and_increment_source_token(data[:token], current_user) tokenizable = source_token.tokenizable QuerySourceToken.validate_source_token_type(source_token) - entities = RetrieveActiveRecordItems.retrieve_from_keys(data, {Supporter => :supporter_id, Nonprofit => :nonprofit_id}) + entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id) - entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, {Campaign => :campaign_id, Event => :event_id, Profile => :profile_id}, true)) + entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true)) validate_entities(entities) @@ -36,10 +36,11 @@ module InsertDonation data = data.except(:old_donation).except('old_donation') result = result.merge(insert_charge(data)) if result['charge']['status'] == 'failed' - raise ChargeError.new(result['charge']['failure_message']) + raise ChargeError, result['charge']['failure_message'] end + # Create the donation record - result['donation'] = self.insert_donation(data, entities) + result['donation'] = insert_donation(data, entities) update_donation_keys(result) result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id]) EmailJobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, result['donation'].id) @@ -63,46 +64,46 @@ module InsertDonation # pass in amount, nonprofit_id, supporter_id, check_number # also pass in offsite_payment sub-hash (can be empty) def self.offsite(data) - ParamValidation.new(data, common_param_validations.merge(offsite_payment: {is_hash: true})) + ParamValidation.new(data, common_param_validations.merge(offsite_payment: { is_hash: true })) - entities = RetrieveActiveRecordItems.retrieve_from_keys(data, {Supporter => :supporter_id, Nonprofit => :nonprofit_id}) - entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, {Campaign => :campaign_id, Event => :event_id, Profile => :profile_id}, true)) + entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id) + entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true)) validate_entities(entities) data = date_from_data(data) - result = {'donation' => self.insert_donation(data.except('offsite_payment'), entities)} - result['payment'] = self.insert_payment('OffsitePayment', 0, result['donation']['id'], data) + result = { 'donation' => insert_donation(data.except('offsite_payment'), entities) } + result['payment'] = insert_payment('OffsitePayment', 0, result['donation']['id'], data) result['offsite_payment'] = Psql.execute( Qexpr.new.insert(:offsite_payments, [ - (data['offsite_payment'] || {}).merge({ - gross_amount: data['amount'], - nonprofit_id: data['nonprofit_id'], - supporter_id: data['supporter_id'], - donation_id: result['donation']['id'], - payment_id: result['payment']['id'], - date: data['date'] - }) - ]).returning('*') + (data['offsite_payment'] || {}).merge( + gross_amount: data['amount'], + nonprofit_id: data['nonprofit_id'], + supporter_id: data['supporter_id'], + donation_id: result['donation']['id'], + payment_id: result['payment']['id'], + date: data['date'] + ) + ]).returning('*') ).first result['activity'] = InsertActivities.for_offsite_donations([result['payment']['id']]) QueueDonations.delay.execute_for_donation(result['donation'].id) - return {status: 200, json: result} + { status: 200, json: result } end def self.with_sepa(data) data = data.with_indifferent_access ParamValidation.new(data, common_param_validations - .merge(direct_debit_detail_id: {required: true, is_reference: true})) + .merge(direct_debit_detail_id: { required: true, is_reference: true })) - entities = RetrieveActiveRecordItems.retrieve_from_keys(data, {Supporter => :supporter_id, Nonprofit => :nonprofit_id}) + entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id) - entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, {Campaign => :campaign_id, Event => :event_id, Profile => :profile_id}, true)) + entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true)) result = {} data[:date] = Time.now result = result.merge(insert_charge(data)) - result['donation'] = self.insert_donation(data, entities) + result['donation'] = insert_donation(data, entities) update_donation_keys(result) EmailJobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, result['donation'].id) @@ -114,44 +115,44 @@ module InsertDonation result end -private + private def self.get_nonprofit_data(nonprofit_id) Psql.execute( Qexpr.new.select(:statement, :name).from(:nonprofits) - .where("id=$id", id: nonprofit_id) + .where('id=$id', id: nonprofit_id) ).first end def self.insert_charge(data) payment_provider = payment_provider(data) nonprofit_data = get_nonprofit_data(data['nonprofit_id']) - kind = data['recurring_donation'] ? "RecurringDonation" : "Donation" + kind = data['recurring_donation'] ? 'RecurringDonation' : 'Donation' if payment_provider == :credit_card - return InsertCharge.with_stripe({ + return InsertCharge.with_stripe( donation_id: data['donation_id'], kind: kind, towards: data['designation'], - metadata: {kind: kind, nonprofit_id: data['nonprofit_id']}, + metadata: { kind: kind, nonprofit_id: data['nonprofit_id'] }, statement: "Donation #{nonprofit_data['statement'] || nonprofit_data['name']}", amount: data['amount'], nonprofit_id: data['nonprofit_id'], supporter_id: data['supporter_id'], card_id: data['card_id'], old_donation: data['old_donation'] ? true : false - }) + ) elsif payment_provider == :sepa - return InsertCharge.with_sepa({ + return InsertCharge.with_sepa( donation_id: data['donation_id'], kind: kind, towards: data['designation'], - metadata: {kind: kind, nonprofit_id: data['nonprofit_id']}, + metadata: { kind: kind, nonprofit_id: data['nonprofit_id'] }, statement: "Donation #{nonprofit_data['statement'] || nonprofit_data['name']}", amount: data['amount'], nonprofit_id: data['nonprofit_id'], supporter_id: data['supporter_id'], direct_debit_detail_id: data['direct_debit_detail_id'] - }) + ) end end @@ -159,17 +160,17 @@ private def self.insert_payment(kind, fee_total, donation_id, data) Psql.execute( Qexpr.new.insert(:payments, [{ - donation_id: donation_id, - gross_amount: data['amount'], - nonprofit_id: data['nonprofit_id'], - supporter_id: data['supporter_id'], - refund_total: 0, - date: data['date'], - towards: data['designation'], - kind: kind, - fee_total: fee_total, - net_amount: data['amount'] - fee_total - }]).returning('*') + donation_id: donation_id, + gross_amount: data['amount'], + nonprofit_id: data['nonprofit_id'], + supporter_id: data['supporter_id'], + refund_total: 0, + date: data['date'], + towards: data['designation'], + kind: kind, + fee_total: fee_total, + net_amount: data['amount'] - fee_total + }]).returning('*') ).first end @@ -202,31 +203,31 @@ private def self.locale_for_supporter(supporter_id) Psql.execute( Qexpr.new.select(:locale).from(:supporters) - .where("id=$id", id: supporter_id) + .where('id=$id', id: supporter_id) ).first['locale'] end def self.payment_provider(data) - if data[:card_id] || data["card_id"] + if data[:card_id] || data['card_id'] :credit_card - elsif data[:direct_debit_detail_id] || data["direct_debit_detail_id"] + elsif data[:direct_debit_detail_id] || data['direct_debit_detail_id'] :sepa end end -def self.parse_date(date) + def self.parse_date(date) date.blank? ? Time.current : Chronic.parse(date) - end + end def self.common_param_validations { - amount: {required: true, is_integer: true}, - nonprofit_id: {required: true, is_reference: true}, - supporter_id: {required: true, is_reference: true}, - designation: {is_a: String}, - dedication: {is_a: String}, - campaign_id: {is_reference: true}, - event_id: {is_reference: true}, + amount: { required: true, is_integer: true }, + nonprofit_id: { required: true, is_reference: true }, + supporter_id: { required: true, is_reference: true }, + designation: { is_a: String }, + dedication: { is_a: String }, + campaign_id: { is_reference: true }, + event_id: { is_reference: true } } end @@ -236,11 +237,11 @@ def self.parse_date(date) raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} is deleted", key: :supporter_id) end - if entities[:event_id] && entities[:event_id].deleted + if entities[:event_id]&.deleted raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} is deleted", key: :event_id) end - if entities[:campaign_id] && entities[:campaign_id].deleted + if entities[:campaign_id]&.deleted raise ParamValidation::ValidationError.new("Campaign #{entities[:campaign_id].id} is deleted", key: :campaign_id) end diff --git a/lib/insert/insert_duplicate.rb b/lib/insert/insert_duplicate.rb index 60e6e6d3..d0c68f07 100644 --- a/lib/insert/insert_duplicate.rb +++ b/lib/insert/insert_duplicate.rb @@ -1,19 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertDuplicate def self.campaign(campaign_id, profile_id) - ParamValidation.new({:campaign_id => campaign_id, :profile_id => profile_id}, - { - :campaign_id => {:required => true, :is_integer => true}, - :profile_id => {:required => true, :is_integer => true} - }) - campaign = Campaign.where("id = ?", campaign_id).first + ParamValidation.new({ campaign_id: campaign_id, profile_id: profile_id }, + campaign_id: { required: true, is_integer: true }, + profile_id: { required: true, is_integer: true }) + campaign = Campaign.where('id = ?', campaign_id).first unless campaign - raise ParamValidation::ValidationError.new("#{campaign_id} is not a valid campaign", {:key => :campaign_id}) + raise ParamValidation::ValidationError.new("#{campaign_id} is not a valid campaign", key: :campaign_id) end - profile = Profile.where("id = ?", profile_id).first + profile = Profile.where('id = ?', profile_id).first unless profile - raise ParamValidation::ValidationError.new("#{profile_id} is not a valid profile", {:key =>:profile_id}) + raise ParamValidation::ValidationError.new("#{profile_id} is not a valid profile", key: :profile_id) end Qx.transaction do @@ -21,7 +21,7 @@ module InsertDuplicate dupe.slug = SlugCopyNamingAlgorithm.new(Campaign, dupe.nonprofit.id).create_copy_name(dupe.slug) dupe.name = NameCopyNamingAlgorithm.new(Campaign, dupe.nonprofit.id).create_copy_name(dupe.name) - if (dupe.end_datetime && dupe.end_datetime.ago(7.days) < DateTime.now) + if dupe.end_datetime && dupe.end_datetime.ago(7.days) < DateTime.now dupe.end_datetime = DateTime.now.since(7.days) end @@ -29,9 +29,17 @@ module InsertDuplicate dupe.save! - dupe.update_attribute(:main_image, campaign.main_image) unless !campaign.main_image rescue AWS::S3::Errors::NoSuchKey + begin + dupe.update_attribute(:main_image, campaign.main_image) if campaign.main_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end - dupe.update_attribute(:background_image, campaign.background_image) unless !campaign.background_image rescue AWS::S3::Errors::NoSuchKey + begin + dupe.update_attribute(:background_image, campaign.background_image) if campaign.background_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end InsertDuplicate.campaign_gift_options(campaign_id, dupe.id) @@ -40,19 +48,17 @@ module InsertDuplicate end def self.event(event_id, profile_id) - ParamValidation.new({:event_id => event_id, :profile_id => profile_id}, - { - :event_id => {:required => true, :is_integer => true}, - :profile_id => {:required => true, :is_integer => true} - }) - event = Event.where("id = ?", event_id).first + ParamValidation.new({ event_id: event_id, profile_id: profile_id }, + event_id: { required: true, is_integer: true }, + profile_id: { required: true, is_integer: true }) + event = Event.where('id = ?', event_id).first unless event - raise ParamValidation::ValidationError.new("#{event_id} is not a valid event", {:key => :event_id}) + raise ParamValidation::ValidationError.new("#{event_id} is not a valid event", key: :event_id) end - profile = Profile.where("id = ?", profile_id).first + profile = Profile.where('id = ?', profile_id).first unless profile - raise ParamValidation::ValidationError.new("#{profile_id} is not a valid profile", {:key =>:profile_id}) + raise ParamValidation::ValidationError.new("#{profile_id} is not a valid profile", key: :profile_id) end Qx.transaction do @@ -63,24 +69,31 @@ module InsertDuplicate we_changed_start_time = false - length_of_event = dupe.end_datetime - dupe.start_datetime - if (dupe.start_datetime.ago(7.days) < DateTime.now) + length_of_event = dupe.end_datetime - dupe.start_datetime + if dupe.start_datetime.ago(7.days) < DateTime.now dupe.start_datetime = DateTime.now.since(7.days) we_changed_start_time = true end - if (we_changed_start_time && dupe.end_datetime) + if we_changed_start_time && dupe.end_datetime dupe.end_datetime = dupe.start_datetime.since(length_of_event) end - dupe.published = false dupe.save! - dupe.update_attribute(:main_image, event.main_image) unless !event.main_image rescue AWS::S3::Errors::NoSuchKey + begin + dupe.update_attribute(:main_image, event.main_image) if event.main_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end - dupe.update_attribute(:background_image, event.background_image) unless !event.background_image rescue AWS::S3::Errors::NoSuchKey + begin + dupe.update_attribute(:background_image, event.background_image) if event.background_image + rescue StandardError + AWS::S3::Errors::NoSuchKey + end InsertDuplicate.ticket_levels(event_id, dupe.id) InsertDuplicate.event_discounts(event_id, dupe.id) @@ -92,58 +105,57 @@ module InsertDuplicate # selects all gift options associated with old campaign # and inserts them and creates associations with a new campaign def self.campaign_gift_options(old_campaign_id, new_campaign_id) - cgos = Qx.select("*") - .from("campaign_gift_options") - .where(campaign_id: old_campaign_id) - .execute - .map {|c| c.except("id", "created_at", "updated_at", "campaign_id") } + cgos = Qx.select('*') + .from('campaign_gift_options') + .where(campaign_id: old_campaign_id) + .execute + .map { |c| c.except('id', 'created_at', 'updated_at', 'campaign_id') } if cgos.any? - return Qx.insert_into("campaign_gift_options") - .values(cgos) - .common_values({campaign_id: new_campaign_id}) - .ts - .returning("*") - .execute + return Qx.insert_into('campaign_gift_options') + .values(cgos) + .common_values(campaign_id: new_campaign_id) + .ts + .returning('*') + .execute end end # selects all ticket levels associated with old event # and inserts them and creates associations with a new event def self.ticket_levels(old_event_id, new_event_id) - tls = Qx.select("*") - .from("ticket_levels") - .where(event_id: old_event_id) - .execute - .map {|t| t.except("id", "created_at", "updated_at", "event_id") } + tls = Qx.select('*') + .from('ticket_levels') + .where(event_id: old_event_id) + .execute + .map { |t| t.except('id', 'created_at', 'updated_at', 'event_id') } if tls.any? - return Qx.insert_into("ticket_levels") - .values(tls) - .common_values({event_id: new_event_id}) - .ts - .returning("*") - .execute + return Qx.insert_into('ticket_levels') + .values(tls) + .common_values(event_id: new_event_id) + .ts + .returning('*') + .execute end end # selects all discounts associated with old event # and inserts them and creates associations with a new event def self.event_discounts(old_event_id, new_event_id) - eds = Qx.select("*") - .from("event_discounts") - .where(event_id: old_event_id) - .execute - .map {|t| t.except("id", "created_at", "updated_at", "event_id") } + eds = Qx.select('*') + .from('event_discounts') + .where(event_id: old_event_id) + .execute + .map { |t| t.except('id', 'created_at', 'updated_at', 'event_id') } if eds.any? - return Qx.insert_into("event_discounts") - .values(eds) - .common_values({event_id: new_event_id}) - .ts - .returning("*") - .execute + return Qx.insert_into('event_discounts') + .values(eds) + .common_values(event_id: new_event_id) + .ts + .returning('*') + .execute end end end - diff --git a/lib/insert/insert_email_lists.rb b/lib/insert/insert_email_lists.rb index 30d04c35..194ed56e 100644 --- a/lib/insert/insert_email_lists.rb +++ b/lib/insert/insert_email_lists.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module InsertEmailLists - def self.for_mailchimp(npo_id, tag_master_ids) # Partial SQL expression for deleting deselected tags delete_expr = Qx.delete_from(:email_lists).where(nonprofit_id: npo_id).returning('mailchimp_list_id') @@ -10,35 +11,33 @@ module InsertEmailLists if tag_master_ids.empty? # no tags were selected; remove all email lists deleted = delete_expr.execute else # Remove all email lists that exist in the db that are not included in tag_master_ids - deleted = delete_expr.where("tag_master_id NOT IN($ids)", ids: tag_master_ids).execute + deleted = delete_expr.where('tag_master_id NOT IN($ids)', ids: tag_master_ids).execute end - mailchimp_lists_to_delete = deleted.map{|h| h['mailchimp_list_id']} + mailchimp_lists_to_delete = deleted.map { |h| h['mailchimp_list_id'] } result = Mailchimp.delete_mailchimp_lists(npo_id, mailchimp_lists_to_delete) - return {deleted: deleted, deleted_result: result} if tag_master_ids.empty? + return { deleted: deleted, deleted_result: result } if tag_master_ids.empty? - existing = Qx.select("tag_master_id").from(:email_lists) - .where(nonprofit_id: npo_id) - .and_where("tag_master_id IN ($ids)", ids: tag_master_ids) - .execute + existing = Qx.select('tag_master_id').from(:email_lists) + .where(nonprofit_id: npo_id) + .and_where('tag_master_id IN ($ids)', ids: tag_master_ids) + .execute tag_master_ids -= existing lists = Mailchimp.create_mailchimp_lists(npo_id, tag_master_ids) - if !lists || !lists.any? || !lists.first[:name] - raise Exception.new("Unable to create mailchimp lists. Response was: #{lists}") + if !lists || lists.none? || !lists.first[:name] + raise Exception, "Unable to create mailchimp lists. Response was: #{lists}" end inserted_lists = Qx.insert_into(:email_lists) - .values( lists.map{|ls| {list_name: ls[:name], mailchimp_list_id: ls[:id], tag_master_id: ls[:tag_master_id]}}) - .common_values({ nonprofit_id: npo_id, }) - .ts - .returning('*') - .execute + .values(lists.map { |ls| { list_name: ls[:name], mailchimp_list_id: ls[:id], tag_master_id: ls[:tag_master_id] } }) + .common_values(nonprofit_id: npo_id) + .ts + .returning('*') + .execute UpdateEmailLists.delay.populate_lists_on_mailchimp(npo_id) - return {deleted: deleted, deleted_result: result, inserted_lists: inserted_lists, inserted_result: lists} + { deleted: deleted, deleted_result: result, inserted_lists: inserted_lists, inserted_result: lists } end - end - diff --git a/lib/insert/insert_full_contact_infos.rb b/lib/insert/insert_full_contact_infos.rb index 7b479390..f32ff447 100644 --- a/lib/insert/insert_full_contact_infos.rb +++ b/lib/insert/insert_full_contact_infos.rb @@ -1,25 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module InsertFullContactInfos - - # Work off of the full_contact_jobs queue def self.work_queue - ids = Qx.select('supporter_id').from('full_contact_jobs').ex.map{|h| h['supporter_id']} + ids = Qx.select('supporter_id').from('full_contact_jobs').ex.map { |h| h['supporter_id'] } Qx.delete_from('full_contact_jobs').where('TRUE').execute - self.bulk(ids) if ids.any? + bulk(ids) if ids.any? end - # Enqueue full contact jobs for a set of supporter ids def self.enqueue(supporter_ids) Qx.insert_into(:full_contact_jobs) - .values(supporter_ids.map{|id| {supporter_id: id}}) + .values(supporter_ids.map { |id| { supporter_id: id } }) .ex end - # We need to throttle our requests by 10ms since that is our rate limit on FullContact def self.bulk(supporter_ids) created_ids = [] @@ -30,9 +28,8 @@ module InsertFullContactInfos interval = 0.1 - (Time.current - now) # account for time taken in .single sleep interval if interval > 0 end - return created_ids + created_ids end - # Fetch and persist a single full contact record for a single supporter # return an exception if 404 or something else went poop @@ -63,54 +60,55 @@ module InsertFullContactInfos age: GetData.hash(result, 'demographics', 'age'), age_range: GetData.hash(result, 'demographics', 'age_range'), location_general: GetData.hash(result, 'demographics', 'location_general'), - websites: (GetData.hash(result, 'contact_info', 'websites') || []).map{|h| h.url}.join(','), + websites: (GetData.hash(result, 'contact_info', 'websites') || []).map(&:url).join(','), supporter_id: supporter_id } - if existing - full_contact_info = Qx.update(:full_contact_infos) - .set(info_data) - .timestamps - .where(id: existing['id']) - .returning('*') - .execute.first - else - full_contact_info = Qx.insert_into(:full_contact_infos) - .values(info_data) - .returning('*') - .timestamps - .execute.first - end + full_contact_info = if existing + Qx.update(:full_contact_infos) + .set(info_data) + .timestamps + .where(id: existing['id']) + .returning('*') + .execute.first + else + Qx.insert_into(:full_contact_infos) + .values(info_data) + .returning('*') + .timestamps + .execute.first + end if result['photos'].present? - photo_data = result['photos'].map{|h| {type_id: h.type_id, url: h.url, is_primary: h.is_primary}} - Qx.delete_from("full_contact_photos") + photo_data = result['photos'].map { |h| { type_id: h.type_id, url: h.url, is_primary: h.is_primary } } + Qx.delete_from('full_contact_photos') .where(full_contact_info_id: full_contact_info['id']) .execute full_contact_photos = Qx.insert_into(:full_contact_photos) - .values(photo_data) - .common_values(full_contact_info_id: full_contact_info['id']) - .timestamps - .returning("*") - .execute + .values(photo_data) + .common_values(full_contact_info_id: full_contact_info['id']) + .timestamps + .returning('*') + .execute end if result['social_profiles'].present? - profile_data = result['social_profiles'].map{|h| {type_id: h.type_id, username: h.username, uid: h.id, bio: h.bio, url: h.url, followers: h.followers, following: h.following} } - Qx.delete_from("full_contact_social_profiles") + profile_data = result['social_profiles'].map { |h| { type_id: h.type_id, username: h.username, uid: h.id, bio: h.bio, url: h.url, followers: h.followers, following: h.following } } + Qx.delete_from('full_contact_social_profiles') .where(full_contact_info_id: full_contact_info['id']) .execute full_contact_social_profiles = Qx.insert_into(:full_contact_social_profiles) - .values(profile_data) - .common_values(full_contact_info_id: full_contact_info['id']) - .timestamps - .returning("*") - .execute + .values(profile_data) + .common_values(full_contact_info_id: full_contact_info['id']) + .timestamps + .returning('*') + .execute end if result['digital_footprint'] && result['digital_footprint']['topics'].present? profile_data = result['social_profiles'] - .map{|h| { + .map do |h| + { type_id: h.type_id, username: h.username, uid: h.id, @@ -118,53 +116,55 @@ module InsertFullContactInfos url: h.url, followers: h.followers, following: h.following - } } - - vals = result['digital_footprint']['topics'].map{|h| h.value} + } + end + + vals = result['digital_footprint']['topics'].map(&:value) existing_vals = Qx.select('value').from('full_contact_topics') - .where("value IN ($vals)", vals: vals) - .and_where("full_contact_info_id=$id", id: full_contact_info['id']) - .execute.map{|h| h['value']} + .where('value IN ($vals)', vals: vals) + .and_where('full_contact_info_id=$id', id: full_contact_info['id']) + .execute.map { |h| h['value'] } topic_data = result['digital_footprint']['topics'] - .reject{|h| existing_vals.include?(h.value)} - .map{|h| {value: h.value, provider: h.provider}} + .reject { |h| existing_vals.include?(h.value) } + .map { |h| { value: h.value, provider: h.provider } } if topic_data.any? full_contact_topics = Qx.insert_into(:full_contact_topics) - .values(topic_data) - .common_values(full_contact_info_id: full_contact_info['id']) - .timestamps - .returning('*') - .execute + .values(topic_data) + .common_values(full_contact_info_id: full_contact_info['id']) + .timestamps + .returning('*') + .execute end end - if result['organizations'].present? Qx.delete_from('full_contact_orgs') .where(full_contact_info_id: full_contact_info['id']) .execute - org_data = result['organizations'].map{|h| { - is_primary: h.is_primary, - name: h.name, - start_date: h.start_date, - end_date: h.end_date, - title: h.title, - current: h.current - } } - .map{|h| h[:end_date] = Format::Date.parse_partial_str(h[:end_date]); h} - .map{|h| h[:start_date] = Format::Date.parse_partial_str(h[:start_date]); h} + org_data = result['organizations'].map do |h| + { + is_primary: h.is_primary, + name: h.name, + start_date: h.start_date, + end_date: h.end_date, + title: h.title, + current: h.current + } + end + .map { |h| h[:end_date] = Format::Date.parse_partial_str(h[:end_date]); h } + .map { |h| h[:start_date] = Format::Date.parse_partial_str(h[:start_date]); h } full_contact_orgs = Qx.insert_into(:full_contact_orgs) - .values(org_data) - .common_values(full_contact_info_id: full_contact_info['id']) - .timestamps - .returning('*') - .execute + .values(org_data) + .common_values(full_contact_info_id: full_contact_info['id']) + .timestamps + .returning('*') + .execute end - return { + { 'full_contact_info' => full_contact_info, 'full_contact_photos' => full_contact_photos, 'full_contact_social_profiles' => full_contact_social_profiles, @@ -176,35 +176,30 @@ module InsertFullContactInfos # Delete all orphaned full contact infos that do not have supporters # or full_contact photos, social_profiles, topics, orgs, etc that do not have a parent info def self.cleanup_orphans - Qx.delete_from("full_contact_infos") - .where("id IN ($ids)", ids: Qx.select("full_contact_infos.id") - .from("full_contact_infos") - .left_join("supporters", "full_contact_infos.supporter_id=supporters.id") - .where("supporters.id IS NULL") - ).ex - Qx.delete_from("full_contact_photos") - .where("id IN ($ids)", ids: Qx.select("full_contact_photos.id") - .from("full_contact_photos") - .left_join("full_contact_infos", "full_contact_infos.id=full_contact_photos.full_contact_info_id") - .where("full_contact_infos.id IS NULL") - ).ex - Qx.delete_from("full_contact_social_profiles") - .where("id IN ($ids)", ids: Qx.select("full_contact_social_profiles.id") - .from("full_contact_social_profiles") - .left_join("full_contact_infos", "full_contact_infos.id=full_contact_social_profiles.full_contact_info_id") - .where("full_contact_infos.id IS NULL") - ).ex - Qx.delete_from("full_contact_topics") - .where("id IN ($ids)", ids: Qx.select("full_contact_topics.id") - .from("full_contact_topics") - .left_join("full_contact_infos", "full_contact_infos.id=full_contact_topics.full_contact_info_id") - .where("full_contact_infos.id IS NULL") - ).ex - Qx.delete_from("full_contact_orgs") - .where("id IN ($ids)", ids: Qx.select("full_contact_orgs.id") - .from("full_contact_orgs") - .left_join("full_contact_infos", "full_contact_infos.id=full_contact_orgs.full_contact_info_id") - .where("full_contact_infos.id IS NULL") - ).ex + Qx.delete_from('full_contact_infos') + .where('id IN ($ids)', ids: Qx.select('full_contact_infos.id') + .from('full_contact_infos') + .left_join('supporters', 'full_contact_infos.supporter_id=supporters.id') + .where('supporters.id IS NULL')).ex + Qx.delete_from('full_contact_photos') + .where('id IN ($ids)', ids: Qx.select('full_contact_photos.id') + .from('full_contact_photos') + .left_join('full_contact_infos', 'full_contact_infos.id=full_contact_photos.full_contact_info_id') + .where('full_contact_infos.id IS NULL')).ex + Qx.delete_from('full_contact_social_profiles') + .where('id IN ($ids)', ids: Qx.select('full_contact_social_profiles.id') + .from('full_contact_social_profiles') + .left_join('full_contact_infos', 'full_contact_infos.id=full_contact_social_profiles.full_contact_info_id') + .where('full_contact_infos.id IS NULL')).ex + Qx.delete_from('full_contact_topics') + .where('id IN ($ids)', ids: Qx.select('full_contact_topics.id') + .from('full_contact_topics') + .left_join('full_contact_infos', 'full_contact_infos.id=full_contact_topics.full_contact_info_id') + .where('full_contact_infos.id IS NULL')).ex + Qx.delete_from('full_contact_orgs') + .where('id IN ($ids)', ids: Qx.select('full_contact_orgs.id') + .from('full_contact_orgs') + .left_join('full_contact_infos', 'full_contact_infos.id=full_contact_orgs.full_contact_info_id') + .where('full_contact_infos.id IS NULL')).ex end end diff --git a/lib/insert/insert_import.rb b/lib/insert/insert_import.rb index 5de38679..e7bad74e 100644 --- a/lib/insert/insert_import.rb +++ b/lib/insert/insert_import.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'required_keys' @@ -9,22 +11,19 @@ require 'insert/insert_custom_field_joins' require 'insert/insert_tag_joins' module InsertImport - # Wrap the import in a transaction and email any errors def self.from_csv_safe(data) - begin - Qx.transaction do - InsertImport.from_csv(data) - end - rescue Exception => e - body = "Import failed. Error: #{e}" - GenericMailer.generic_mail( - 'support@commitchange.com', 'Jay Bot', # FROM - body, - 'Import error', # SUBJECT - 'support@commitchange.com', 'Jay' # TO - ).deliver + Qx.transaction do + InsertImport.from_csv(data) end + rescue Exception => e + body = "Import failed. Error: #{e}" + GenericMailer.generic_mail( + 'support@commitchange.com', 'Jay Bot', # FROM + body, + 'Import error', # SUBJECT + 'support@commitchange.com', 'Jay' # TO + ).deliver end # Insert a bunch of Supporter and related data using a CSV and a bunch of header_matches @@ -33,22 +32,21 @@ module InsertImport # data: nonprofit_id, user_email, user_id, file, header_matches # Will send a notification email to user_email when the import is completed def self.from_csv(data) - ParamValidation.new(data, { - file_uri: {required: true}, - header_matches: {required: true}, - nonprofit_id: {required: true, is_integer: true}, - user_email: {required: true} - }) + ParamValidation.new(data, + file_uri: { required: true }, + header_matches: { required: true }, + nonprofit_id: { required: true, is_integer: true }, + user_email: { required: true }) import = Qx.insert_into(:imports) - .values({ - date: Time.current, - nonprofit_id: data[:nonprofit_id], - user_id: data[:user_id] - }) - .timestamps - .returning('*') - .execute.first + .values( + date: Time.current, + nonprofit_id: data[:nonprofit_id], + user_id: data[:user_id] + ) + .timestamps + .returning('*') + .execute.first row_count = 0 imported_count = 0 supporter_ids = [] @@ -59,9 +57,10 @@ module InsertImport CSV.new(open(data[:file_uri]), headers: :first_row).each do |row| row_count += 1 # triplet of [header_name, value, import_key] - matches = row.map{|key, val| [key, val, data[:header_matches][key]]} + matches = row.map { |key, val| [key, val, data[:header_matches][key]] } next if matches.empty? - table_data = matches.reduce({}) do |acc, triplet| + + table_data = matches.each_with_object({}) do |triplet, acc| key, val, match = triplet if match == 'custom_field' acc['custom_fields'] ||= [] @@ -76,7 +75,6 @@ module InsertImport acc[table][col] = val end end - acc end # Create supporter record @@ -96,11 +94,11 @@ module InsertImport if table_data['supporter']['id'] && table_data['custom_fields'] && table_data['custom_fields'].any? InsertCustomFieldJoins.find_or_create(data[:nonprofit_id], [table_data['supporter']['id']], table_data['custom_fields']) end - + # Create new tags if table_data['supporter']['id'] && table_data['tags'] && table_data['tags'].any? # Split tags by semicolons - tags = table_data['tags'].select{|t| t.present?}.map{|t| t.split(/[;,]/).map(&:strip)}.flatten + tags = table_data['tags'].select(&:present?).map { |t| t.split(/[;,]/).map(&:strip) }.flatten InsertTagJoins.find_or_create(data[:nonprofit_id], [table_data['supporter']['id']], tags) end @@ -119,7 +117,7 @@ module InsertImport # Create payment record if table_data['donation'] && table_data['donation']['id'] - table_data['payment'] = Qx.insert_into(:payments).values({ + table_data['payment'] = Qx.insert_into(:payments).values( gross_amount: table_data['donation']['amount'], fee_total: 0, net_amount: table_data['donation']['amount'], @@ -129,8 +127,8 @@ module InsertImport donation_id: table_data['donation']['id'], towards: table_data['donation']['designation'], date: table_data['donation']['date'] - }).ts.returning('*') - .execute.first + ).ts.returning('*') + .execute.first imported_count += 1 else table_data['payment'] = {} @@ -138,7 +136,7 @@ module InsertImport # Create offsite payment record if table_data['donation'] && table_data['donation']['id'] - table_data['offsite_payment'] = Qx.insert_into(:offsite_payments).values({ + table_data['offsite_payment'] = Qx.insert_into(:offsite_payments).values( gross_amount: table_data['donation']['amount'], check_number: GetData.chain(table_data['offsite_payment'], 'check_number'), kind: table_data['offsite_payment'] && table_data['offsite_payment']['check_number'] ? 'check' : '', @@ -147,8 +145,8 @@ module InsertImport donation_id: table_data['donation']['id'], payment_id: table_data['payment']['id'], date: table_data['donation']['date'] - }).ts.returning('*') - .execute.first + ).ts.returning('*') + .execute.first imported_count += 1 else table_data['offsite_payment'] = {} @@ -161,12 +159,12 @@ module InsertImport InsertActivities.for_offsite_donations(created_payment_ids) if created_payment_ids.count > 0 import = Qx.update(:imports) - .set(row_count: row_count, imported_count: imported_count) - .where(id: import['id']) - .returning('*') - .execute.first + .set(row_count: row_count, imported_count: imported_count) + .where(id: import['id']) + .returning('*') + .execute.first InsertFullContactInfos.enqueue(supporter_ids) if supporter_ids.any? ImportMailer.delay.import_completed_notification(import['id']) - return import + import end end diff --git a/lib/insert/insert_nonprofit_keys.rb b/lib/insert/insert_nonprofit_keys.rb index 10dc47db..c1b70160 100644 --- a/lib/insert/insert_nonprofit_keys.rb +++ b/lib/insert/insert_nonprofit_keys.rb @@ -1,35 +1,35 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'httparty' require 'cypher' - module InsertNonprofitKeys include HTTParty - def self.insert_mailchimp_access_token(npo_id, code) - form_data = "grant_type=authorization_code&client_id=#{URI.escape ENV['MAILCHIMP_OAUTH_CLIENT_ID']}&client_secret=#{ENV['MAILCHIMP_OAUTH_CLIENT_SECRET']}&redirect_uri=#{ENV['MAILCHIMP_REDIRECT_URL']}%2Fmailchimp-landing&code=#{URI.escape code}" + def self.insert_mailchimp_access_token(npo_id, code) + form_data = "grant_type=authorization_code&client_id=#{URI.escape ENV['MAILCHIMP_OAUTH_CLIENT_ID']}&client_secret=#{ENV['MAILCHIMP_OAUTH_CLIENT_SECRET']}&redirect_uri=#{ENV['MAILCHIMP_REDIRECT_URL']}%2Fmailchimp-landing&code=#{URI.escape code}" - response = post('https://login.mailchimp.com/oauth2/token', { body: form_data }) - if response['error'] - raise Exception.new(response['error']) - end - response['access_token'] = Cypher.encrypt(response['access_token']) + response = post('https://login.mailchimp.com/oauth2/token', body: form_data) + raise Exception, response['error'] if response['error'] - key_row_id = Qx.select("*") - .from(:nonprofit_keys).where(nonprofit_id: npo_id) - .execute.map{|h| h['id']}.first + response['access_token'] = Cypher.encrypt(response['access_token']) - if key_row_id.nil? - Qx.insert_into(:nonprofit_keys) - .values({nonprofit_id: npo_id, mailchimp_token: response['access_token'].to_json}) - .ts.execute - else - Qx.update(:nonprofit_keys) - .set(mailchimp_token: response['access_token']) - .ts.where({'id' => key_row_id}) - .execute - end + key_row_id = Qx.select('*') + .from(:nonprofit_keys).where(nonprofit_id: npo_id) + .execute.map { |h| h['id'] }.first - return response['access_token'] + if key_row_id.nil? + Qx.insert_into(:nonprofit_keys) + .values(nonprofit_id: npo_id, mailchimp_token: response['access_token'].to_json) + .ts.execute + else + Qx.update(:nonprofit_keys) + .set(mailchimp_token: response['access_token']) + .ts.where('id' => key_row_id) + .execute end + + response['access_token'] + end end diff --git a/lib/insert/insert_payout.rb b/lib/insert/insert_payout.rb index bfc6eabe..b8a52fd2 100644 --- a/lib/insert/insert_payout.rb +++ b/lib/insert/insert_payout.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Create a new payout @@ -10,82 +12,83 @@ require 'update/update_disputes' require 'param_validation' module InsertPayout - # Pass in the following inside the data hash: # - stripe_account_id # - email # - user_ip # - bank_name # options hash can have a :date (before date) for only paying out funds before a certain date (useful for only disbursing the prev month) - def self.with_stripe(np_id, data, options={}) - bigger_data = (data ? data : {}).merge(np_id: np_id) - ParamValidation.new(bigger_data, { - np_id: {required: true, is_integer: true}, - stripe_account_id: {not_blank: true, required: true}, - email: {not_blank: true, required: true}, - user_ip: {not_blank: true, required: true}, - bank_name: {not_blank: true, required: true} - }) + def self.with_stripe(np_id, data, options = {}) + bigger_data = (data || {}).merge(np_id: np_id) + ParamValidation.new(bigger_data, + np_id: { required: true, is_integer: true }, + stripe_account_id: { not_blank: true, required: true }, + email: { not_blank: true, required: true }, + user_ip: { not_blank: true, required: true }, + bank_name: { not_blank: true, required: true }) options ||= {} entities = RetrieveActiveRecordItems.retrieve_from_keys(bigger_data, Nonprofit => :np_id) payment_ids = QueryPayments.ids_for_payout(np_id, options) if payment_ids.count < 1 - raise ArgumentError.new("No payments are available for disbursal on this account.") + raise ArgumentError, 'No payments are available for disbursal on this account.' end + totals = QueryPayments.get_payout_totals(payment_ids) nonprofit_currency = entities[:np_id].currency now = Time.current begin stripe_transfer = StripeUtils.create_transfer(totals['net_amount'], data[:stripe_account_id], nonprofit_currency) - Psql.transaction do - # Create the Transfer on Stripe + Psql.transaction do + # Create the Transfer on Stripe - # Retrieve all payments with available charges and undisbursed refunds - # Mark all the above payments as disbursed - UpdateCharges.disburse_all_with_payments(payment_ids) - # Mark all the above refunds as disbursed - UpdateRefunds.disburse_all_with_payments(payment_ids) - # Mark all disputes as lost_and_paid - UpdateDisputes.disburse_all_with_payments(payment_ids) - # Get gross total, total fees, net total, and total count - # Create the payout record (whether it succeeded on Stripe or not) - payout = Psql.execute( - Qexpr.new.insert(:payouts, [{ - net_amount: totals['net_amount'], - nonprofit_id: np_id, - failure_message: stripe_transfer['failure_message'], - status: stripe_transfer.status, - fee_total: totals['fee_total'], - gross_amount: totals['gross_amount'], - email: data[:email], - count: totals['count'], - stripe_transfer_id: stripe_transfer.id, - user_ip: data[:user_ip], - ach_fee: 0, - bank_name: data[:bank_name]}]) - .returning('id', 'net_amount', 'nonprofit_id', 'created_at', 'updated_at', 'status', 'fee_total', 'gross_amount', 'email', 'count', 'stripe_transfer_id', 'user_ip', 'ach_fee', 'bank_name') - ).first - # Create PaymentPayout records linking all the payments to the payout - pps = Psql.execute(Qexpr.new.insert('payment_payouts', payment_ids.map{|id| {payment_id: id.to_i}}, {common_data: {payout_id: payout['id'].to_i}})) - NonprofitMailer.delay.pending_payout_notification(payout['id'].to_i) + # Retrieve all payments with available charges and undisbursed refunds + # Mark all the above payments as disbursed + UpdateCharges.disburse_all_with_payments(payment_ids) + # Mark all the above refunds as disbursed + UpdateRefunds.disburse_all_with_payments(payment_ids) + # Mark all disputes as lost_and_paid + UpdateDisputes.disburse_all_with_payments(payment_ids) + # Get gross total, total fees, net total, and total count + # Create the payout record (whether it succeeded on Stripe or not) + payout = Psql.execute( + Qexpr.new.insert(:payouts, [{ + net_amount: totals['net_amount'], + nonprofit_id: np_id, + failure_message: stripe_transfer['failure_message'], + status: stripe_transfer.status, + fee_total: totals['fee_total'], + gross_amount: totals['gross_amount'], + email: data[:email], + count: totals['count'], + stripe_transfer_id: stripe_transfer.id, + user_ip: data[:user_ip], + ach_fee: 0, + bank_name: data[:bank_name] + }]) + .returning('id', 'net_amount', 'nonprofit_id', 'created_at', 'updated_at', 'status', 'fee_total', 'gross_amount', 'email', 'count', 'stripe_transfer_id', 'user_ip', 'ach_fee', 'bank_name') + ).first + # Create PaymentPayout records linking all the payments to the payout + pps = Psql.execute(Qexpr.new.insert('payment_payouts', payment_ids.map { |id| { payment_id: id.to_i } }, common_data: { payout_id: payout['id'].to_i })) + NonprofitMailer.delay.pending_payout_notification(payout['id'].to_i) return payout end rescue Stripe::StripeError => e payout = Psql.execute( - Qexpr.new.insert(:payouts, [{ - net_amount: totals['net_amount'], - nonprofit_id: np_id, - failure_message: e.message, - status: 'failed', - fee_total: totals['fee_total'], - gross_amount: totals['gross_amount'], - email: data[:email], - count: totals['count'], - stripe_transfer_id: nil, - user_ip: data[:user_ip], - ach_fee: 0, - bank_name: data[:bank_name]}]) - .returning('id', 'net_amount', 'nonprofit_id', 'created_at', 'updated_at', 'status', 'fee_total', 'gross_amount', 'email', 'count', 'stripe_transfer_id', 'user_ip', 'ach_fee', 'bank_name') + Qexpr.new.insert(:payouts, [{ + net_amount: totals['net_amount'], + nonprofit_id: np_id, + failure_message: e.message, + status: 'failed', + fee_total: totals['fee_total'], + gross_amount: totals['gross_amount'], + email: data[:email], + count: totals['count'], + stripe_transfer_id: nil, + user_ip: data[:user_ip], + ach_fee: 0, + bank_name: data[:bank_name] + }]) + .returning('id', 'net_amount', 'nonprofit_id', 'created_at', 'updated_at', 'status', 'fee_total', 'gross_amount', 'email', 'count', 'stripe_transfer_id', 'user_ip', 'ach_fee', 'bank_name') ).first return payout end diff --git a/lib/insert/insert_recurring_donation.rb b/lib/insert/insert_recurring_donation.rb index fc2acba6..021fdfc6 100644 --- a/lib/insert/insert_recurring_donation.rb +++ b/lib/insert/insert_recurring_donation.rb @@ -1,41 +1,40 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertRecurringDonation - # Create a recurring_donation, donation, payment, charge, and activity # See controllers/nonprofits/recurring_donations_controller#create for the data params to pass in def self.with_stripe(data) data = data.with_indifferent_access ParamValidation.new(data, InsertDonation.common_param_validations - .merge(token: {required: true, format: UUID::Regex})) + .merge(token: { required: true, format: UUID::Regex })) - unless data[:recurring_donation].nil? + if data[:recurring_donation].nil? + data[:recurring_donation] = {} + else - ParamValidation.new(data[:recurring_donation], { - interval: {is_integer: true}, - start_date: {can_be_date: true}, - time_unit: {included_in: %w(month day week year)}, - paydate: {is_integer:true} - }) - if (data[:recurring_donation][:paydate]) + ParamValidation.new(data[:recurring_donation], + interval: { is_integer: true }, + start_date: { can_be_date: true }, + time_unit: { included_in: %w[month day week year] }, + paydate: { is_integer: true }) + if data[:recurring_donation][:paydate] data[:recurring_donation][:paydate] = data[:recurring_donation][:paydate].to_i end - ParamValidation.new(data[:recurring_donation], { - paydate: {min:1, max:28} - }) + ParamValidation.new(data[:recurring_donation], + paydate: { min: 1, max: 28 }) - else - data[:recurring_donation] = {} end source_token = QuerySourceToken.get_and_increment_source_token(data[:token], nil) tokenizable = source_token.tokenizable QuerySourceToken.validate_source_token_type(source_token) - entities = RetrieveActiveRecordItems.retrieve_from_keys(data, {Supporter => :supporter_id, Nonprofit => :nonprofit_id}) + entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id) - entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, {Campaign => :campaign_id, Event => :event_id, Profile => :profile_id}, true)) + entities = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, { Campaign => :campaign_id, Event => :event_id, Profile => :profile_id }, true)) InsertDonation.validate_entities(entities) @@ -52,10 +51,10 @@ module InsertRecurringDonation data = data.except(:old_donation).except('old_donation') # if start date is today, make initial charge first test_start_date = get_test_start_date(data) - if test_start_date == nil || Time.current >= test_start_date + if test_start_date.nil? || Time.current >= test_start_date result = result.merge(InsertDonation.insert_charge(data)) if result['charge']['status'] == 'failed' - raise ChargeError.new(result['charge']['failure_message']) + raise ChargeError, result['charge']['failure_message'] end end @@ -63,7 +62,7 @@ module InsertRecurringDonation result['donation'] = InsertDonation.insert_donation(data, entities) entities[:donation_id] = result['donation'] # Create the recurring_donation record - result['recurring_donation'] = insert_recurring_donation(data,entities) + result['recurring_donation'] = insert_recurring_donation(data, entities) # Update charge foreign keys if result['payment'] InsertDonation.update_donation_keys(result) @@ -74,10 +73,10 @@ module InsertRecurringDonation EmailJobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, result['donation'].id) EmailJobQueue.queue(JobTypes::DonorPaymentNotificationJob, result['donation'].id, entities[:supporter_id].locale) QueueDonations.delay.execute_for_donation(result['donation']['id']) - return result + result end -def self.with_sepa(data) + def self.with_sepa(data) data = set_defaults(data) data = data.merge(payment_provider: payment_provider(data)) result = {} @@ -87,16 +86,14 @@ def self.with_sepa(data) end result['donation'] = Psql.execute(Qexpr.new.insert(:donations, [ - data.except(:recurring_donation) - ]).returning('*')).first + data.except(:recurring_donation) + ]).returning('*')).first result['recurring_donation'] = Psql.execute(Qexpr.new.insert(:recurring_donations, [ - data[:recurring_donation].merge(donation_id: result['donation']['id']) - ]).returning('*')).first + data[:recurring_donation].merge(donation_id: result['donation']['id']) + ]).returning('*')).first - if result['payment'] - InsertDonation.update_donation_keys(result) - end + InsertDonation.update_donation_keys(result) if result['payment'] DonationMailer.delay.nonprofit_payment_notification(result['donation']['id']) DonationMailer.delay.donor_direct_debit_notification(result['donation']['id'], locale_for_supporter(result['donation']['supporter_id'])) @@ -104,21 +101,21 @@ def self.with_sepa(data) QueueDonations.delay.execute_for_donation(result['donation']['id']) { status: 200, json: result } - end - - - # the data model here is brutal. This needs to get cleaned up. - def self.convert_donation_to_recurring_donation(donation_id) - ParamValidation.new({donation_id: donation_id}, {donation_id: {:required => true, :is_integer => true}}) - don = Donation.where('id = ? ', donation_id).first - if !don - raise ParamValidation::ValidationError.new("#{donation_id} is not a valid donation", {:key => :donation_id, :val => donation_id}) end - rd = insert_recurring_donation({amount:don.amount, email: don.supporter.email, anonymous: don.anonymous, origin_url: don.origin_url, recurring_donation: { start_date: don.created_at, :paydate => convert_date_to_valid_paydate(don.created_at)}, date: don.created_at}, {supporter_id: don.supporter, nonprofit_id: don.nonprofit, donation_id: don}) + + # the data model here is brutal. This needs to get cleaned up. + def self.convert_donation_to_recurring_donation(donation_id) + ParamValidation.new({ donation_id: donation_id }, donation_id: { required: true, is_integer: true }) + don = Donation.where('id = ? ', donation_id).first + unless don + raise ParamValidation::ValidationError.new("#{donation_id} is not a valid donation", key: :donation_id, val: donation_id) + end + + rd = insert_recurring_donation({ amount: don.amount, email: don.supporter.email, anonymous: don.anonymous, origin_url: don.origin_url, recurring_donation: { start_date: don.created_at, paydate: convert_date_to_valid_paydate(don.created_at) }, date: don.created_at }, supporter_id: don.supporter, nonprofit_id: don.nonprofit, donation_id: don) don.recurring_donation = rd don.recurring = true - don.payment.kind = "RecurringDonation" + don.payment.kind = 'RecurringDonation' don.payment.save! rd.save! don.save! @@ -126,7 +123,7 @@ def self.with_sepa(data) rd end - def self.insert_recurring_donation(data, entities) + def self.insert_recurring_donation(data, entities) rd = RecurringDonation.new rd.amount = data[:amount] rd.nonprofit = entities[:nonprofit_id] @@ -134,17 +131,17 @@ def self.with_sepa(data) rd.supporter_id = entities[:supporter_id].id rd.active = true rd.edit_token = SecureRandom.uuid - rd.n_failures= 0 - rd.email= entities[:supporter_id].email - rd.interval = data[:recurring_donation][:interval].blank? ? 1 : data[:recurring_donation][:interval] - rd.time_unit= data[:recurring_donation][:time_unit].blank? ? 'month' : data[:recurring_donation][:time_unit] - if data[:recurring_donation][:start_date].blank? - rd.start_date= Time.current.beginning_of_day - elsif data[:recurring_donation][:start_date].is_a? Time - rd.start_date = data[:recurring_donation][:start_date] - else - rd.start_date = Chronic.parse(data[:recurring_donation][:start_date]) - end + rd.n_failures = 0 + rd.email = entities[:supporter_id].email + rd.interval = data[:recurring_donation][:interval].blank? ? 1 : data[:recurring_donation][:interval] + rd.time_unit = data[:recurring_donation][:time_unit].blank? ? 'month' : data[:recurring_donation][:time_unit] + rd.start_date = if data[:recurring_donation][:start_date].blank? + Time.current.beginning_of_day + elsif data[:recurring_donation][:start_date].is_a? Time + data[:recurring_donation][:start_date] + else + Chronic.parse(data[:recurring_donation][:start_date]) + end if rd.time_unit == 'month' && rd.interval == 1 && data[:recurring_donation][:paydate].nil? rd.paydate = convert_date_to_valid_paydate(rd.start_date) @@ -154,22 +151,20 @@ def self.with_sepa(data) rd.save! rd - end -def self.get_test_start_date(data) + end + + def self.get_test_start_date(data) unless data[:recurring_donation] && data[:recurring_donation][:start_date] return nil end - return Chronic.parse(data[:recurring_donation][:start_date]) - - - end - + Chronic.parse(data[:recurring_donation][:start_date]) + end def self.locale_for_supporter(supporter_id) Psql.execute( Qexpr.new.select(:locale).from(:supporters) - .where("id=$id", id: supporter_id) + .where('id=$id', id: supporter_id) ).first['locale'] end @@ -183,6 +178,6 @@ def self.get_test_start_date(data) def self.convert_date_to_valid_paydate(date) day = date.day - return day > 28 ? 28 : day + day > 28 ? 28 : day end end diff --git a/lib/insert/insert_refunds.rb b/lib/insert/insert_refunds.rb index c50a7b47..aa589994 100644 --- a/lib/insert/insert_refunds.rb +++ b/lib/insert/insert_refunds.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'format/currency' require 'validation_error' @@ -10,30 +12,28 @@ require 'param_validation' require 'insert/insert_activities' module InsertRefunds - # Refund a given charge, up to its net amount # params: amount, donation obj def self.with_stripe(charge, h) - ParamValidation.new(charge, { - payment_id: {required: true, is_integer: true}, - stripe_charge_id: {required: true, format: /^(test_)?ch_.*$/}, - amount: {required: true, is_integer: true, min: 1}, - id: {required: true, is_integer: true}, - nonprofit_id: {required: true, is_integer: true}, - supporter_id: {required: true, is_integer: true} - }) - ParamValidation.new(h, { amount: {required: true, is_integer: true, min: 1} }) - - original_payment = Qx.select("*").from("payments").where(id: charge['payment_id']).execute.first - raise ActiveRecord::RecordNotFound.new("Cannot find original payment for refund on charge #{charge['id']}") if original_payment.nil? + ParamValidation.new(charge, + payment_id: { required: true, is_integer: true }, + stripe_charge_id: { required: true, format: /^(test_)?ch_.*$/ }, + amount: { required: true, is_integer: true, min: 1 }, + id: { required: true, is_integer: true }, + nonprofit_id: { required: true, is_integer: true }, + supporter_id: { required: true, is_integer: true }) + ParamValidation.new(h, amount: { required: true, is_integer: true, min: 1 }) + + original_payment = Qx.select('*').from('payments').where(id: charge['payment_id']).execute.first + raise ActiveRecord::RecordNotFound, "Cannot find original payment for refund on charge #{charge['id']}" if original_payment.nil? if original_payment['refund_total'].to_i + h['amount'].to_i > original_payment['gross_amount'].to_i - raise RuntimeError.new("Refund amount must be less than the net amount of the payment (for charge #{charge['id']})") + raise "Refund amount must be less than the net amount of the payment (for charge #{charge['id']})" end stripe_charge = Stripe::Charge.retrieve(charge['stripe_charge_id']) - refund_post_data = {'amount' => h['amount'], 'refund_application_fee' => true, 'reverse_transfer' => true} + refund_post_data = { 'amount' => h['amount'], 'refund_application_fee' => true, 'reverse_transfer' => true } refund_post_data['reason'] = h['reason'] unless h['reason'].blank? # Stripe will error on blank reason field stripe_refund = stripe_charge.refunds.create(refund_post_data) h['stripe_refund_id'] = stripe_refund.id @@ -45,19 +45,19 @@ module InsertRefunds fees = (h['amount'] * -original_payment['fee_total'] / original_payment['gross_amount']).ceil net = gross + fees # Create a corresponding negative payment record - payment_row = Qx.insert_into(:payments).values({ - gross_amount: gross, - fee_total: fees, - net_amount: net, - kind: 'Refund', - towards: original_payment['towards'], - date: refund_row['created_at'], - nonprofit_id: charge['nonprofit_id'], - supporter_id: charge['supporter_id'] - }) - .timestamps - .returning('*') - .execute.first + payment_row = Qx.insert_into(:payments).values( + gross_amount: gross, + fee_total: fees, + net_amount: net, + kind: 'Refund', + towards: original_payment['towards'], + date: refund_row['created_at'], + nonprofit_id: charge['nonprofit_id'], + supporter_id: charge['supporter_id'] + ) + .timestamps + .returning('*') + .execute.first InsertActivities.for_refunds([payment_row['id']]) @@ -68,8 +68,6 @@ module InsertRefunds # Send the refund receipts in a delayed job Delayed::Job.enqueue JobTypes::DonorRefundNotificationJob.new(refund_row['id']) Delayed::Job.enqueue JobTypes::NonprofitRefundNotificationJob.new(refund_row['id']) - return {'payment' => payment_row, 'refund' => refund_row} + { 'payment' => payment_row, 'refund' => refund_row } end - end - diff --git a/lib/insert/insert_source_token.rb b/lib/insert/insert_source_token.rb index ece45da2..a9e92a67 100644 --- a/lib/insert/insert_source_token.rb +++ b/lib/insert/insert_source_token.rb @@ -1,14 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertSourceToken - - def self.create_record(tokenizable, params={}) - ParamValidation.new({tokenizable:tokenizable}.merge(params), { - tokenizable: {required:true}, - event: {is_a: Event}, - expiration_time: {is_integer: true, min: 1}, - max_uses: {is_integer: true, min: 1} - }) - if (params[:event] != nil) + def self.create_record(tokenizable, params = {}) + ParamValidation.new({ tokenizable: tokenizable }.merge(params), + tokenizable: { required: true }, + event: { is_a: Event }, + expiration_time: { is_integer: true, min: 1 }, + max_uses: { is_integer: true, min: 1 }) + if !params[:event].nil? max_uses = params[:max_uses] || Settings.source_tokens.event_donation_source.max_uses expiration_diff = params[:expiration_time] || Settings.source_tokens.event_donation_source.time_after_event expiration = params[:event].end_datetime + expiration_diff.to_i @@ -26,6 +26,4 @@ module InsertSourceToken c.save! c end - - -end \ No newline at end of file +end diff --git a/lib/insert/insert_supporter.rb b/lib/insert/insert_supporter.rb index 594b93a3..a41eebdf 100644 --- a/lib/insert/insert_supporter.rb +++ b/lib/insert/insert_supporter.rb @@ -1,58 +1,57 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' require 'i18n' module InsertSupporter - - def self.create_or_update(np_id, data, update=false) - ParamValidation.new(data.merge(np_id: np_id), { - np_id: {required: true, is_integer: true} - }) - address_keys = ['name', 'address', 'city', 'country', 'state_code'] + def self.create_or_update(np_id, data, update = false) + ParamValidation.new(data.merge(np_id: np_id), + np_id: { required: true, is_integer: true }) + address_keys = %w[name address city country state_code] custom_fields = data['customFields'] data = HashWithIndifferentAccess.new(Format::RemoveDiacritics.from_hash(data, address_keys)) - .except(:customFields) + .except(:customFields) - supporter = Qx.select("*").from(:supporters) - .where("name = $n AND email = $e", n: data[:name], e: data[:email]) - .and_where("nonprofit_id=$id", id: np_id) - .and_where("coalesce(deleted, FALSE)=FALSE") - .execute.last - if supporter and update + supporter = Qx.select('*').from(:supporters) + .where('name = $n AND email = $e', n: data[:name], e: data[:email]) + .and_where('nonprofit_id=$id', id: np_id) + .and_where('coalesce(deleted, FALSE)=FALSE') + .execute.last + if supporter && update supporter = Qx.update(:supporters) - .set(defaults(data)) - .where("id=$id", id: supporter['id']) - .returning('*') - .timestamps - .execute.last - else + .set(defaults(data)) + .where('id=$id', id: supporter['id']) + .returning('*') + .timestamps + .execute.last + else supporter = Qx.insert_into(:supporters) - .values(defaults(data).merge(nonprofit_id: np_id)) - .returning('*') - .timestamps - .execute.last - end - - if custom_fields - InsertCustomFieldJoins.find_or_create(np_id, [supporter['id']], custom_fields) + .values(defaults(data).merge(nonprofit_id: np_id)) + .returning('*') + .timestamps + .execute.last end - #GeocodeModel.delay.supporter(supporter['id']) + if custom_fields + InsertCustomFieldJoins.find_or_create(np_id, [supporter['id']], custom_fields) + end + + # GeocodeModel.delay.supporter(supporter['id']) InsertFullContactInfos.enqueue([supporter['id']]) - return supporter + supporter end - - def self.defaults(h) + def self.defaults(h) h = h.except('profile_id') unless h['profile_id'].present? - if h['first_name'].present? || h['last_name'].present? - h['name'] = h['first_name'] || h['last_name'] - if h['first_name'] && h['last_name'] - h['name'] = "#{h['first_name'].strip} #{h['last_name'].strip}" - end - end + if h['first_name'].present? || h['last_name'].present? + h['name'] = h['first_name'] || h['last_name'] + if h['first_name'] && h['last_name'] + h['name'] = "#{h['first_name'].strip} #{h['last_name'].strip}" + end + end h['email_unsubscribe_uuid'] = SecureRandom.uuid @@ -62,8 +61,8 @@ module InsertSupporter h = h.except('address_line2') - return h - end + h + end # pass in a hash of supporter info, as well as # any property with tag_x will create a tag with name 'name' @@ -78,15 +77,14 @@ module InsertSupporter # The above will create a supporter with name/email, one tag with name 'xy', # and one field with name 'xy' and value 420 def self.with_tags_and_fields(np_id, data) - tags = data.select{|key, val| key.match(/^tag_/)}.map{|key, val| key.gsub('tag_', '')} - fields = data.select{|key, val| key.match(/^field_/)}.map{|key, val| [key.gsub('field_', ''), val]} - supp_cols = data.select{|key, val| !key.match(/^field_/) && !key.match(/^tag_/)} + tags = data.select { |key, _val| key.match(/^tag_/) }.map { |key, _val| key.gsub('tag_', '') } + fields = data.select { |key, _val| key.match(/^field_/) }.map { |key, val| [key.gsub('field_', ''), val] } + supp_cols = data.select { |key, _val| !key.match(/^field_/) && !key.match(/^tag_/) } supporter = create_or_update(np_id, supp_cols) InsertTagJoins.delay.find_or_create(np_id, [supporter['id']], tags) if tags.any? InsertCustomFieldJoins.delay.find_or_create(np_id, [supporter['id']], fields) if fields.any? - return supporter + supporter end - end diff --git a/lib/insert/insert_supporter_notes.rb b/lib/insert/insert_supporter_notes.rb index 003f2d06..f868c624 100644 --- a/lib/insert/insert_supporter_notes.rb +++ b/lib/insert/insert_supporter_notes.rb @@ -1,22 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'param_validation' require 'qx' module InsertSupporterNotes def self.create(notes) - ParamValidation.new(notes, { - root: { array_of_hashes: { - supporter_id: {required: true, is_integer: true}, - user_id: {required: true, is_integer: true}, - content: {required: true} - } } - }) + ParamValidation.new(notes, + root: { array_of_hashes: { + supporter_id: { required: true, is_integer: true }, + user_id: { required: true, is_integer: true }, + content: { required: true } + } }) inserted = Qx.insert_into(:supporter_notes) - .values(notes) - .timestamps - .returning('*') - .execute - InsertActivities.for_supporter_notes(inserted.map{|h| h['id']}) - return inserted + .values(notes) + .timestamps + .returning('*') + .execute + InsertActivities.for_supporter_notes(inserted.map { |h| h['id'] }) + inserted end end diff --git a/lib/insert/insert_tag_joins.rb b/lib/insert/insert_tag_joins.rb index 11fa284e..a7286ee4 100644 --- a/lib/insert/insert_tag_joins.rb +++ b/lib/insert/insert_tag_joins.rb @@ -1,10 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qx' module InsertTagJoins - - # @param [Integer] np_id id for a [Nonprofit] # @param [Integer] profile_id id for the [Profile] corresponding to the current user. Not used currently but needed # @param [Array] supporter_ids the ids of the all the supporters whose tags should be changed. @@ -15,69 +15,64 @@ module InsertTagJoins def self.in_bulk(np_id, profile_id, supporter_ids, tag_data) begin ParamValidation.new({ - np_id: np_id, - profile_id: profile_id, - supporter_ids: supporter_ids, - tag_data: tag_data - }, { - np_id: {required: true, is_integer: true}, - profile_id: {required: true, is_integer: true}, - supporter_ids: {is_array: true}, - tag_data: { required: true - # array_of_hashes: { - # selected: {required: true}, tag_master_id: {required: true, is_integer: true} - # } - } - }) + np_id: np_id, + profile_id: profile_id, + supporter_ids: supporter_ids, + tag_data: tag_data + }, + np_id: { required: true, is_integer: true }, + profile_id: { required: true, is_integer: true }, + supporter_ids: { is_array: true }, + tag_data: { required: true }) + # array_of_hashes: { + # selected: {required: true}, tag_master_id: {required: true, is_integer: true} + # } rescue ParamValidation::ValidationError => e - return {json: {error: "Validation error\n #{e.message}", errors: e.data}, status: :unprocessable_entity} + return { json: { error: "Validation error\n #{e.message}", errors: e.data }, status: :unprocessable_entity } end begin - return {json: {error: "Nonprofit #{np_id} is not valid"}, status: :unprocessable_entity} unless Nonprofit.exists?(np_id) - return {json: {error: "Profile #{profile_id} is not valid"}, status: :unprocessable_entity} unless Profile.exists?(profile_id) - + return { json: { error: "Nonprofit #{np_id} is not valid" }, status: :unprocessable_entity } unless Nonprofit.exists?(np_id) + return { json: { error: "Profile #{profile_id} is not valid" }, status: :unprocessable_entity } unless Profile.exists?(profile_id) # verify that the supporters belong to the nonprofit original_supporter_request = supporter_ids.count supporter_ids = Supporter.where('nonprofit_id = ? and id IN (?)', np_id, supporter_ids).pluck(:id) unless supporter_ids.any? - return {json: {inserted_count: 0, removed_count: 0}, status: :ok} + return { json: { inserted_count: 0, removed_count: 0 }, status: :ok } end # filtering the tag_data to this nonprofit - valid_ids = TagMaster.where('nonprofit_id = ? and id IN (?)', np_id, tag_data.map {|tg| tg[:tag_master_id] }).pluck(:id).to_a - filtered_tag_data = tag_data.select {|i| valid_ids.include? i[:tag_master_id].to_i} - - + valid_ids = TagMaster.where('nonprofit_id = ? and id IN (?)', np_id, tag_data.map { |tg| tg[:tag_master_id] }).pluck(:id).to_a + filtered_tag_data = tag_data.select { |i| valid_ids.include? i[:tag_master_id].to_i } # first, delete the items which should be removed - to_remove = filtered_tag_data.select{|t| !t[:selected]}.map{|t| t[:tag_master_id]} + to_remove = filtered_tag_data.reject { |t| t[:selected] }.map { |t| t[:tag_master_id] } deleted = [] if to_remove.any? deleted = Qx.delete_from(:tag_joins) - .where("supporter_id IN ($ids)", ids: supporter_ids) - .and_where("tag_master_id in ($tags)", tags: to_remove) - .returning('*') - .execute + .where('supporter_id IN ($ids)', ids: supporter_ids) + .and_where('tag_master_id in ($tags)', tags: to_remove) + .returning('*') + .execute end # next add only the selected tag_joins - to_insert = filtered_tag_data.select{|t| t[:selected]}.map{|t| t[:tag_master_id]} - insert_data = supporter_ids.map{|id| to_insert.map{|tag_master_id| {supporter_id: id, tag_master_id: tag_master_id}}}.flatten + to_insert = filtered_tag_data.select { |t| t[:selected] }.map { |t| t[:tag_master_id] } + insert_data = supporter_ids.map { |id| to_insert.map { |tag_master_id| { supporter_id: id, tag_master_id: tag_master_id } } }.flatten if insert_data.any? tags = Qx.insert_into(:tag_joins) - .values(insert_data) - .timestamps - .on_conflict() - .conflict_columns(:supporter_id, :tag_master_id).upsert(:tag_join_supporter_unique_idx) - .returning('*') - .execute + .values(insert_data) + .timestamps + .on_conflict + .conflict_columns(:supporter_id, :tag_master_id).upsert(:tag_join_supporter_unique_idx) + .returning('*') + .execute else tags = [] end rescue ActiveRecord::ActiveRecordError => e - return {json: {error: "A DB error occurred. Please contact support. \n #{e.message}"}, status: :unprocessable_entity} + return { json: { error: "A DB error occurred. Please contact support. \n #{e.message}" }, status: :unprocessable_entity } end # Create an activity for the modified tags for every supporter @@ -88,40 +83,35 @@ module InsertTagJoins # Sync mailchimp lists, if present Mailchimp.delay.sync_supporters_to_list_from_tag_joins(np_id, supporter_ids, tag_data) - return {json: {inserted_count: tags.count, removed_count: deleted.count }, status: :ok} + { json: { inserted_count: tags.count, removed_count: deleted.count }, status: :ok } end - # Find or create many tag names for every supporter # Creates tag masters for tag names that are not present def self.find_or_create(np_id, supporter_ids, tag_names) # Pair each tag name with a tag master id tags = tag_names.map do |name| tm = Qx.select(:id).from(:tag_masters) - .where(name: name) - .and_where(nonprofit_id: np_id) - .execute.last - if !tm - tm = Qx.insert_into(:tag_masters).values({ - name: name, - nonprofit_id: np_id - }).ts.returning('id').execute.last - end + .where(name: name) + .and_where(nonprofit_id: np_id) + .execute.last + tm ||= Qx.insert_into(:tag_masters).values( + name: name, + nonprofit_id: np_id + ).ts.returning('id').execute.last [name, tm['id']] end tag_join_data = supporter_ids.map do |id| - tags.map{|name, tm_id| {supporter_id: id, tag_master_id: tm_id}} + tags.map { |_name, tm_id| { supporter_id: id, tag_master_id: tm_id } } end.flatten tag_joins = Qx.insert_into(:tag_joins) - .values(tag_join_data) - .ts.returning('id').execute + .values(tag_join_data) + .ts.returning('id').execute - return tag_joins + tag_joins end private - - end diff --git a/lib/insert/insert_tickets.rb b/lib/insert/insert_tickets.rb index e1ca13d6..bb9529cb 100644 --- a/lib/insert/insert_tickets.rb +++ b/lib/insert/insert_tickets.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertTickets - # Will generate rows for payment, offsite_payment or charge, tickets, activities # pass in: # data: { @@ -16,32 +17,31 @@ module InsertTickets # } def self.create(data) data = data.with_indifferent_access - ParamValidation.new(data, { - tickets: {required: true, is_array: true}, - nonprofit_id: {required: true, is_reference: true}, - supporter_id: {required: true, is_reference: true}, - event_id: {required: true, is_reference: true}, - event_discount_id: {is_reference: true}, - kind: {included_in: ['free', 'charge', 'offsite']}, - token: {format: UUID::Regex}, - offsite_payment: {is_hash: true} - }) + ParamValidation.new(data, + tickets: { required: true, is_array: true }, + nonprofit_id: { required: true, is_reference: true }, + supporter_id: { required: true, is_reference: true }, + event_id: { required: true, is_reference: true }, + event_discount_id: { is_reference: true }, + kind: { included_in: %w[free charge offsite] }, + token: { format: UUID::Regex }, + offsite_payment: { is_hash: true }) - data[:tickets].each {|t| - ParamValidation.new(t, {quantity: {is_integer: true, required: true, min: 1}, ticket_level_id: {is_reference: true, required: true}}) - } + data[:tickets].each do |t| + ParamValidation.new(t, quantity: { is_integer: true, required: true, min: 1 }, ticket_level_id: { is_reference: true, required: true }) + end - ParamValidation.new(data[:offsite_payment], {kind: {included_in: %w(cash check)}}) if data[:offsite_payment] && !data[:offsite_payment][:kind].blank? + ParamValidation.new(data[:offsite_payment], kind: { included_in: %w[cash check] }) if data[:offsite_payment] && !data[:offsite_payment][:kind].blank? - entities = RetrieveActiveRecordItems.retrieve_from_keys(data, {Supporter => :supporter_id, Nonprofit => :nonprofit_id, Event => :event_id}) + entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Supporter => :supporter_id, Nonprofit => :nonprofit_id, Event => :event_id) - entities.merge!(RetrieveActiveRecordItems.retrieve_from_keys(data, {EventDiscount => :event_discount_id}, true)) + entities.merge!(RetrieveActiveRecordItems.retrieve_from_keys(data, { EventDiscount => :event_discount_id }, true)) tl_entities = get_ticket_level_entities(data) validate_entities(entities, tl_entities) - #verify that enough tickets_available + # verify that enough tickets_available QueryTicketLevels.verify_tickets_available(data[:tickets]) gross_amount = QueryTicketLevels.gross_amount_from_tickets(data[:tickets], data[:event_discount_id]) @@ -63,28 +63,29 @@ module InsertTickets # Create charge for tickets elsif data['kind'] == 'charge' || !data['kind'] - source_token = QuerySourceToken.get_and_increment_source_token(data[:token],nil) + source_token = QuerySourceToken.get_and_increment_source_token(data[:token], nil) QuerySourceToken.validate_source_token_type(source_token) tokenizable = source_token.tokenizable ## does the card belong to the supporter? if tokenizable.holder != entities[:supporter_id] raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not own card #{tokenizable.id}", key: :token) end - result = result.merge(InsertCharge.with_stripe({ - kind: "Ticket", - towards: entities[:event_id].name, - metadata: {kind: "Ticket", event_id: entities[:event_id].id, nonprofit_id: entities[:nonprofit_id].id}, - statement: "Tickets #{entities[:event_id].name}", - amount: gross_amount, - nonprofit_id: entities[:nonprofit_id].id, - supporter_id: entities[:supporter_id].id, - card_id: tokenizable.id - })) + + result = result.merge(InsertCharge.with_stripe( + kind: 'Ticket', + towards: entities[:event_id].name, + metadata: { kind: 'Ticket', event_id: entities[:event_id].id, nonprofit_id: entities[:nonprofit_id].id }, + statement: "Tickets #{entities[:event_id].name}", + amount: gross_amount, + nonprofit_id: entities[:nonprofit_id].id, + supporter_id: entities[:supporter_id].id, + card_id: tokenizable.id + )) if result['charge']['status'] == 'failed' - raise ChargeError.new(result['charge']['failure_message']) + raise ChargeError, result['charge']['failure_message'] end else - raise ParamValidation::ValidationError.new("Ticket costs money but you didn't pay.", {key: :kind}) + raise ParamValidation::ValidationError.new("Ticket costs money but you didn't pay.", key: :kind) end end @@ -94,29 +95,29 @@ module InsertTickets result['tickets'] = generated_ticket_entities(data['tickets'], result, entities) # Create the activity rows for the tickets - InsertActivities.for_tickets(result['tickets'].map{|t| t.id}) + InsertActivities.for_tickets(result['tickets'].map(&:id)) - ticket_ids = result['tickets'].map{|t| t.id} + ticket_ids = result['tickets'].map(&:id) charge_id = result['charge'] ? result['charge'].id : nil EmailJobQueue.queue(JobTypes::TicketMailerReceiptAdminJob, ticket_ids) EmailJobQueue.queue(JobTypes::TicketMailerFollowupJob, ticket_ids, charge_id) - return result - end - + result + end # Generate a set of 'bid ids' (ids for each ticket scoped within the event) def self.generate_bid_ids(event_id, tickets) # Generate the bid ids last_bid_id = Psql.execute( - Qexpr.new.select("COUNT(*)").from(:tickets) - .where("event_id=$id", id: event_id)).first['count'].to_i - tickets.zip(last_bid_id + 1 .. last_bid_id + tickets.count).map{|h, id| h.merge('bid_id' => id)} + Qexpr.new.select('COUNT(*)').from(:tickets) + .where('event_id=$id', id: event_id) + ).first['count'].to_i + tickets.zip(last_bid_id + 1..last_bid_id + tickets.count).map { |h, id| h.merge('bid_id' => id) } end - #not really needed but used for breaking into the unit test and getting the IDs + # not really needed but used for breaking into the unit test and getting the IDs def self.generated_ticket_entities(ticket_data, result, entities) - ticket_data.map{|ticket_request| + ticket_data.map do |ticket_request| t = Ticket.new t.quantity = ticket_request['quantity'] t.ticket_level = ticket_request['ticket_level_id'] @@ -128,7 +129,7 @@ module InsertTickets t.event_discount = entities[:event_discount_id] t.save! t - }.to_a + end.to_a end def self.validate_entities(entities, tl_entities) @@ -141,8 +142,8 @@ module InsertTickets raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} is deleted", key: :event_id) end - #verify that enough tickets_available - tl_entities.each {|i| + # verify that enough tickets_available + tl_entities.each do |i| if i[:ticket_level_id].deleted raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} is deleted", key: :tickets) end @@ -150,7 +151,7 @@ module InsertTickets if i[:ticket_level_id].event != entities[:event_id] raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} does not belong to event #{entities[:event_id]}", key: :tickets) end - } + end # Does the supporter belong to the nonprofit? if entities[:supporter_id].nonprofit != entities[:nonprofit_id] @@ -168,35 +169,35 @@ module InsertTickets end def self.get_ticket_level_entities(data) - data[:tickets].map{|i| + data[:tickets].map do |i| { - quantity: i[:quantity], - ticket_level_id: RetrieveActiveRecordItems.retrieve_from_keys(i, TicketLevel => :ticket_level_id)[:ticket_level_id] + quantity: i[:quantity], + ticket_level_id: RetrieveActiveRecordItems.retrieve_from_keys(i, TicketLevel => :ticket_level_id)[:ticket_level_id] } - }.to_a + end.to_a end def self.create_payment(entities, gross_amount) p = Payment.new - p.gross_amount= gross_amount - p.nonprofit= entities[:nonprofit_id] - p.supporter= entities[:supporter_id] - p.refund_total= 0 + p.gross_amount = gross_amount + p.nonprofit = entities[:nonprofit_id] + p.supporter = entities[:supporter_id] + p.refund_total = 0 p.date = Time.current p.towards = entities[:event_id].name p.fee_total = 0 p.net_amount = gross_amount - p.kind= "OffsitePayment" + p.kind = 'OffsitePayment' p.save! p end def self.create_offsite_payment(entities, gross_amount, data, payment) p = OffsitePayment.new - p.gross_amount= gross_amount - p.nonprofit= entities[:nonprofit_id] - p.supporter= entities[:supporter_id] - p.date= Time.current + p.gross_amount = gross_amount + p.nonprofit = entities[:nonprofit_id] + p.supporter = entities[:supporter_id] + p.date = Time.current p.payment = payment p.kind = data['offsite_payment']['kind'] p.check_number = data['offsite_payment']['check_number'] diff --git a/lib/insert/insert_tracking.rb b/lib/insert/insert_tracking.rb index dc5dc3ff..45b8a69b 100644 --- a/lib/insert/insert_tracking.rb +++ b/lib/insert/insert_tracking.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module InsertTracking def self.create(params) result = {} result['tracking'] = Qx.insert_into(:trackings) - .values({ - utm_campaign: params[:utm_campaign], - utm_content: params[:utm_content], - utm_medium: params[:utm_medium], - utm_source: params[:utm_source], - donation_id: params[:donation_id] - }) - .timestamps - .returning('*') - .execute.first + .values( + utm_campaign: params[:utm_campaign], + utm_content: params[:utm_content], + utm_medium: params[:utm_medium], + utm_source: params[:utm_source], + donation_id: params[:donation_id] + ) + .timestamps + .returning('*') + .execute.first { status: 200, json: result } end diff --git a/lib/job_types/admin_failed_gift_job.rb b/lib/job_types/admin_failed_gift_job.rb index aa90f2ed..d7da2959 100644 --- a/lib/job_types/admin_failed_gift_job.rb +++ b/lib/job_types/admin_failed_gift_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class AdminFailedGiftJob < EmailJob @@ -12,4 +14,4 @@ module JobTypes AdminMailer.notify_failed_gift(@donation, @campaign_gift_option).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/admin_notice_job.rb b/lib/job_types/admin_notice_job.rb index a16720f1..54b09bf8 100644 --- a/lib/job_types/admin_notice_job.rb +++ b/lib/job_types/admin_notice_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class AdminNoticeJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes GenericMailer.admin_notice(@options).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/campaign_creation_followup_job.rb b/lib/job_types/campaign_creation_followup_job.rb index cb627e0a..6c92b870 100644 --- a/lib/job_types/campaign_creation_followup_job.rb +++ b/lib/job_types/campaign_creation_followup_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class CampaignCreationFollowupJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes CampaignMailer.creation_followup(@campaign).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/donor_direct_debit_notification_job.rb b/lib/job_types/donor_direct_debit_notification_job.rb index dccf0fe0..84b94706 100644 --- a/lib/job_types/donor_direct_debit_notification_job.rb +++ b/lib/job_types/donor_direct_debit_notification_job.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class DonorDirectDebitNotificationJob < EmailJob attr_reader :donation_id - def initialize(donation_id, locale=I18n.locale) + def initialize(donation_id, locale = I18n.locale) @donation_id = donation_id @locale = locale end @@ -12,4 +14,4 @@ module JobTypes DonationMailer.donor_direct_debit_notification(@donation_id, @locale).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/donor_failed_recurring_donation_job.rb b/lib/job_types/donor_failed_recurring_donation_job.rb index 9b29bdd4..272ab40e 100644 --- a/lib/job_types/donor_failed_recurring_donation_job.rb +++ b/lib/job_types/donor_failed_recurring_donation_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class DonorFailedRecurringDonationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes DonationMailer.donor_failed_recurring_donation(@donation_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/donor_payment_notification_job.rb b/lib/job_types/donor_payment_notification_job.rb index 490bbaab..ca85280f 100644 --- a/lib/job_types/donor_payment_notification_job.rb +++ b/lib/job_types/donor_payment_notification_job.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class DonorPaymentNotificationJob < EmailJob - attr_reader :donation_id - def initialize(donation_id, locale=I18n.locale) - @donation_id = donation_id - @locale = locale - end + class DonorPaymentNotificationJob < EmailJob + attr_reader :donation_id + def initialize(donation_id, locale = I18n.locale) + @donation_id = donation_id + @locale = locale + end - def perform - DonationMailer.donor_payment_notification(@donation_id, @locale).deliver - end - end -end \ No newline at end of file + def perform + DonationMailer.donor_payment_notification(@donation_id, @locale).deliver + end + end +end diff --git a/lib/job_types/donor_recurring_donation_change_amount_job.rb b/lib/job_types/donor_recurring_donation_change_amount_job.rb index 4cb920fb..6fa6de5b 100644 --- a/lib/job_types/donor_recurring_donation_change_amount_job.rb +++ b/lib/job_types/donor_recurring_donation_change_amount_job.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class DonorRecurringDonationChangeAmountJob < EmailJob - attr_reader :donation_id, :previous_amount - def initialize(donation_id, previous_amount=nil) - @donation_id = donation_id - @previous_amount = previous_amount - end + class DonorRecurringDonationChangeAmountJob < EmailJob + attr_reader :donation_id, :previous_amount + def initialize(donation_id, previous_amount = nil) + @donation_id = donation_id + @previous_amount = previous_amount + end - def perform - DonationMailer.donor_recurring_donation_change_amount(@donation_id, @previous_amount).deliver - end - end -end \ No newline at end of file + def perform + DonationMailer.donor_recurring_donation_change_amount(@donation_id, @previous_amount).deliver + end + end +end diff --git a/lib/job_types/donor_refund_notification_job.rb b/lib/job_types/donor_refund_notification_job.rb index 8cc4357e..1343427b 100644 --- a/lib/job_types/donor_refund_notification_job.rb +++ b/lib/job_types/donor_refund_notification_job.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class DonorRefundNotificationJob < EmailJob - attr_reader :refund_id - def initialize(refund_id) - @refund_id = refund_id - end + class DonorRefundNotificationJob < EmailJob + attr_reader :refund_id + def initialize(refund_id) + @refund_id = refund_id + end - def perform - UserMailer.refund_receipt(@refund_id).deliver - end - end -end \ No newline at end of file + def perform + UserMailer.refund_receipt(@refund_id).deliver + end + end +end diff --git a/lib/job_types/email_job.rb b/lib/job_types/email_job.rb index e5811637..d238ca29 100644 --- a/lib/job_types/email_job.rb +++ b/lib/job_types/email_job.rb @@ -1,27 +1,28 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class EmailJob - def perform - raise 'You need to override this' - end + class EmailJob + def perform + raise 'You need to override this' + end - def max_attempts - MAX_EMAIL_JOB_ATTEMPTS || 1 - end + def max_attempts + MAX_EMAIL_JOB_ATTEMPTS || 1 + end - def destroy_failed_jobs? - false - end + def destroy_failed_jobs? + false + end - def error(job, exception) - end + def error(job, exception); end - def reschedule_at(current_time, attempts) - current_time + attempts**(2.195); - end + def reschedule_at(current_time, attempts) + current_time + attempts**2.195 + end - def queue_name - 'email_queue' - end - end -end \ No newline at end of file + def queue_name + 'email_queue' + end + end +end diff --git a/lib/job_types/event_creation_followup_job.rb b/lib/job_types/event_creation_followup_job.rb index 9141f3c2..cea71321 100644 --- a/lib/job_types/event_creation_followup_job.rb +++ b/lib/job_types/event_creation_followup_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class EventCreationFollowupJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes EventMailer.creation_followup(@event).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_payment_completed_job.rb b/lib/job_types/export_payment_completed_job.rb index ad60f392..c8d92482 100644 --- a/lib/job_types/export_payment_completed_job.rb +++ b/lib/job_types/export_payment_completed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportPaymentCompletedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_payments_completed_notification(export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_payment_failed_job.rb b/lib/job_types/export_payment_failed_job.rb index bb1ffeaf..991cace8 100644 --- a/lib/job_types/export_payment_failed_job.rb +++ b/lib/job_types/export_payment_failed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportPaymentFailedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_payments_failed_notification(export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_recurring_donations_completed_job.rb b/lib/job_types/export_recurring_donations_completed_job.rb index 56f4ca1f..8871a91d 100644 --- a/lib/job_types/export_recurring_donations_completed_job.rb +++ b/lib/job_types/export_recurring_donations_completed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportRecurringDonationsCompletedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_recurring_donations_completed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_recurring_donations_failed_job.rb b/lib/job_types/export_recurring_donations_failed_job.rb index dcfa69fb..705fbfa1 100644 --- a/lib/job_types/export_recurring_donations_failed_job.rb +++ b/lib/job_types/export_recurring_donations_failed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportRecurringDonationsFailedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_recurring_donations_failed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_supporter_notes_completed_job.rb b/lib/job_types/export_supporter_notes_completed_job.rb index 31ccd056..10542128 100644 --- a/lib/job_types/export_supporter_notes_completed_job.rb +++ b/lib/job_types/export_supporter_notes_completed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportSupporterNotesCompletedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_supporter_notes_completed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_supporter_notes_failed_job.rb b/lib/job_types/export_supporter_notes_failed_job.rb index 40652dc1..1579836b 100644 --- a/lib/job_types/export_supporter_notes_failed_job.rb +++ b/lib/job_types/export_supporter_notes_failed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ExportSupporterNotesFailedJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ExportMailer.export_supporter_notes_failed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_supporters_completed_job.rb b/lib/job_types/export_supporters_completed_job.rb index 7c9b09da..84e1a452 100644 --- a/lib/job_types/export_supporters_completed_job.rb +++ b/lib/job_types/export_supporters_completed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module JobTypes class ExportSupportersCompletedJob < EmailJob attr_reader :export @@ -10,4 +12,4 @@ module JobTypes ExportMailer.export_supporters_completed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/export_supporters_failed_job.rb b/lib/job_types/export_supporters_failed_job.rb index e7555f9e..f8c7cfc9 100644 --- a/lib/job_types/export_supporters_failed_job.rb +++ b/lib/job_types/export_supporters_failed_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module JobTypes class ExportSupportersFailedJob < EmailJob attr_reader :export @@ -10,4 +12,4 @@ module JobTypes ExportMailer.export_supporters_failed_notification(@export).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/generic_mail_job.rb b/lib/job_types/generic_mail_job.rb index 1a7738a5..121baba0 100644 --- a/lib/job_types/generic_mail_job.rb +++ b/lib/job_types/generic_mail_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class GenericMailJob < EmailJob @@ -16,4 +18,4 @@ module JobTypes GenericMailer.generic_mail(@from_email, @from_name, @message, @subject, @to_email, @to_name).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/import_complete_notification_job.rb b/lib/job_types/import_complete_notification_job.rb index fca866b7..1ae54972 100644 --- a/lib/job_types/import_complete_notification_job.rb +++ b/lib/job_types/import_complete_notification_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class ImportCompleteNotificationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes ImportMailer.import_completed_notification(@import_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_admin_existing_invite_job.rb b/lib/job_types/nonprofit_admin_existing_invite_job.rb index 2f2a3322..0eae9123 100644 --- a/lib/job_types/nonprofit_admin_existing_invite_job.rb +++ b/lib/job_types/nonprofit_admin_existing_invite_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitAdminExistingInviteJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitAdminMailer.existing_invite(@role).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_admin_new_invite_job.rb b/lib/job_types/nonprofit_admin_new_invite_job.rb index c0d03dea..dcccbfe6 100644 --- a/lib/job_types/nonprofit_admin_new_invite_job.rb +++ b/lib/job_types/nonprofit_admin_new_invite_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitAdminNewInviteJob < EmailJob @@ -12,4 +14,4 @@ module JobTypes NonprofitAdminMailer.new_invite(@role, @raw_token).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb b/lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb index 031fd930..a4a2937d 100644 --- a/lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb +++ b/lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitAdminSupporterFundraiserJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitAdminMailer.supporter_fundraiser(@event_or_campaign).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_failed_recurring_donation_job.rb b/lib/job_types/nonprofit_failed_recurring_donation_job.rb index bb3f2f53..76bf5ce9 100644 --- a/lib/job_types/nonprofit_failed_recurring_donation_job.rb +++ b/lib/job_types/nonprofit_failed_recurring_donation_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitFailedRecurringDonationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes DonationMailer.nonprofit_failed_recurring_donation(@donation_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_failed_verification_job.rb b/lib/job_types/nonprofit_failed_verification_job.rb index ba4e71b4..68fb717e 100644 --- a/lib/job_types/nonprofit_failed_verification_job.rb +++ b/lib/job_types/nonprofit_failed_verification_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitFailedVerificationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitMailer.failed_verification_notice(@np).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_new_bank_account_job.rb b/lib/job_types/nonprofit_new_bank_account_job.rb index b7688f7c..55a2adca 100644 --- a/lib/job_types/nonprofit_new_bank_account_job.rb +++ b/lib/job_types/nonprofit_new_bank_account_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitNewBankAccountJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitMailer.new_bank_account_notification(@ba).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_payment_notification_job.rb b/lib/job_types/nonprofit_payment_notification_job.rb index 3936115e..5d328e03 100644 --- a/lib/job_types/nonprofit_payment_notification_job.rb +++ b/lib/job_types/nonprofit_payment_notification_job.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class NonprofitPaymentNotificationJob < EmailJob - attr_reader :donation_id, :user_id - def initialize(donation_id, user_id=nil) - @donation_id = donation_id - @user_id = user_id - end + class NonprofitPaymentNotificationJob < EmailJob + attr_reader :donation_id, :user_id + def initialize(donation_id, user_id = nil) + @donation_id = donation_id + @user_id = user_id + end - def perform - DonationMailer.nonprofit_payment_notification(@donation_id, @user_id).deliver - end - end -end \ No newline at end of file + def perform + DonationMailer.nonprofit_payment_notification(@donation_id, @user_id).deliver + end + end +end diff --git a/lib/job_types/nonprofit_pending_payout_job.rb b/lib/job_types/nonprofit_pending_payout_job.rb index 662e3f35..827db759 100644 --- a/lib/job_types/nonprofit_pending_payout_job.rb +++ b/lib/job_types/nonprofit_pending_payout_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitPendingPayoutJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitMailer.pending_payout_notification(@payout_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_recurring_donation_cancellation_job.rb b/lib/job_types/nonprofit_recurring_donation_cancellation_job.rb index 0f0513d6..2a33b52a 100644 --- a/lib/job_types/nonprofit_recurring_donation_cancellation_job.rb +++ b/lib/job_types/nonprofit_recurring_donation_cancellation_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitRecurringDonationCancellationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes DonationMailer.nonprofit_recurring_donation_cancellation(@donation_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_recurring_donation_change_amount_job.rb b/lib/job_types/nonprofit_recurring_donation_change_amount_job.rb index c1209e40..35528a93 100644 --- a/lib/job_types/nonprofit_recurring_donation_change_amount_job.rb +++ b/lib/job_types/nonprofit_recurring_donation_change_amount_job.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitRecurringDonationChangeAmountJob < EmailJob attr_reader :donation_id, :previous_amount - def initialize(donation_id, previous_amount=nil) + def initialize(donation_id, previous_amount = nil) @donation_id = donation_id @previous_amount = previous_amount end @@ -12,4 +14,4 @@ module JobTypes DonationMailer.nonprofit_recurring_donation_change_amount(@donation_id, @previous_amount).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_refund_notification_job.rb b/lib/job_types/nonprofit_refund_notification_job.rb index 9531c858..ea08ae3a 100644 --- a/lib/job_types/nonprofit_refund_notification_job.rb +++ b/lib/job_types/nonprofit_refund_notification_job.rb @@ -1,13 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes - class NonprofitRefundNotificationJob < EmailJob - attr_reader :refund_id - def initialize(refund_id) - @refund_id = refund_id - end + class NonprofitRefundNotificationJob < EmailJob + attr_reader :refund_id + def initialize(refund_id) + @refund_id = refund_id + end - def perform - NonprofitMailer.refund_notification(@refund_id).deliver - end - end -end \ No newline at end of file + def perform + NonprofitMailer.refund_notification(@refund_id).deliver + end + end +end diff --git a/lib/job_types/nonprofit_successful_verification_job.rb b/lib/job_types/nonprofit_successful_verification_job.rb index d11ae13c..a19109ef 100644 --- a/lib/job_types/nonprofit_successful_verification_job.rb +++ b/lib/job_types/nonprofit_successful_verification_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitSuccessfulVerificationJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitMailer.successful_verification_notice(@np).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/nonprofit_welcome_job.rb b/lib/job_types/nonprofit_welcome_job.rb index dd0fb773..8c13bdb1 100644 --- a/lib/job_types/nonprofit_welcome_job.rb +++ b/lib/job_types/nonprofit_welcome_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class NonprofitWelcomeJob < EmailJob @@ -11,4 +13,4 @@ module JobTypes NonprofitMailer.welcome(@nonprofit_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/ticket_mailer_followup_job.rb b/lib/job_types/ticket_mailer_followup_job.rb index e7749058..09ea2f78 100644 --- a/lib/job_types/ticket_mailer_followup_job.rb +++ b/lib/job_types/ticket_mailer_followup_job.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class TicketMailerFollowupJob < EmailJob @@ -12,4 +14,4 @@ module JobTypes TicketMailer.followup(@ticket_ids, @charge_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/job_types/ticket_mailer_receipt_admin_job.rb b/lib/job_types/ticket_mailer_receipt_admin_job.rb index b65c2fe2..ba0f2911 100644 --- a/lib/job_types/ticket_mailer_receipt_admin_job.rb +++ b/lib/job_types/ticket_mailer_receipt_admin_job.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module JobTypes class TicketMailerReceiptAdminJob < EmailJob attr_reader :ticket_ids - def initialize(ticket_ids, user_id=nil) + def initialize(ticket_ids, user_id = nil) @ticket_ids = ticket_ids @user_id = user_id end @@ -12,4 +14,4 @@ module JobTypes TicketMailer.receipt_admin(@ticket_ids, @user_id).deliver end end -end \ No newline at end of file +end diff --git a/lib/json_resp.rb b/lib/json_resp.rb index d5dd39b0..b2f528f4 100644 --- a/lib/json_resp.rb +++ b/lib/json_resp.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Provide a declarative json validation and error responses for the rails 'render' method in controllers # @@ -9,7 +11,6 @@ # * Respond with proper codes and error messages for everything class JsonResp - attr_accessor :errors def initialize(params, &block) @@ -17,36 +18,35 @@ class JsonResp validation = JsonResp::Validation.new(params) validation.instance_exec(params, &block) @errors = validation.errors - return self + self end - def when_valid(&block) - return {status: 422, json: {errors: @errors}} if @errors.any? + def when_valid + return { status: 422, json: { errors: @errors } } if @errors.any? + begin - @response = block.call(@params) + @response = yield(@params) rescue Exception => e - @response = {status: 500, json: {error: "We're sorry, but something went wrong. We've been notified about this issue."}} + @response = { status: 500, json: { error: "We're sorry, but something went wrong. We've been notified about this issue." } } puts e puts e.backtrace.first(10) end - return @response + @response end - # Validation of a set of request parameters class Validation - attr_accessor :errors, :params def initialize(params) @params = params @errors = [] - return self + self end def requires(*keys) - @errors.concat keys.select{|k| @params[k].blank? }.map{|k| "#{k} required"} - return Param.new(keys, @errors, @params) + @errors.concat keys.select { |k| @params[k].blank? }.map { |k| "#{k} required" } + Param.new(keys, @errors, @params) end def requires_either(key1, key2) @@ -54,16 +54,15 @@ class JsonResp if @params[key1].blank? && @params[key2].blank? @errors << error_message else - @errors.concat [key1, key2].select{|k| @params[k].blank? }.map{|k| "#{k} required"} + @errors.concat [key1, key2].select { |k| @params[k].blank? }.map { |k| "#{k} required" } end - return Param.new([key1, key2], @errors, @params) + Param.new([key1, key2], @errors, @params) end def optional(*keys) - keys_to_check = keys.select{|k| @params[k].present?} - return Param.new(keys_to_check, @errors, @params) + keys_to_check = keys.select { |k| @params[k].present? } + Param.new(keys_to_check, @errors, @params) end - end class Param @@ -74,67 +73,66 @@ class JsonResp attr_accessor :keys, :errors, :params def initialize(keys, errors, params) - @keys = keys.reject{|k| params[k].nil? } + @keys = keys.reject { |k| params[k].nil? } @errors = errors @params = params end def as_string - @errors.concat @keys.reject{|k| @params[k].is_a?(String)}.map{|k| "#{k} must be a string"} - return self + @errors.concat @keys.reject { |k| @params[k].is_a?(String) }.map { |k| "#{k} must be a string" } + self end def as_int @errors.concat @keys - .reject{|k| @params[k].is_a?(Integer) || @params[k].to_i.to_s == @params[k]} - .map{|k| "#{k} must be an integer"} - return self + .reject { |k| @params[k].is_a?(Integer) || @params[k].to_i.to_s == @params[k] } + .map { |k| "#{k} must be an integer" } + self end def with_format(regex) - @errors.concat @keys.reject{|k| @params[k] =~ regex}.map{|k| "#{k} must match: #{regex}"} - return self + @errors.concat @keys.reject { |k| @params[k] =~ regex }.map { |k| "#{k} must match: #{regex}" } + self end def one_of(*vals) - @errors.concat @keys.reject{|k| vals.include?(@params[k])}.map{|k| "#{k} must be one of: #{vals.join(", ")}"} - return self + @errors.concat @keys.reject { |k| vals.include?(@params[k]) }.map { |k| "#{k} must be one of: #{vals.join(', ')}" } + self end def nested(&block) - @errors.concat @keys.map{|k| Validation.new(@params[k]).instance_exec(@params, &block).errors}.flatten - return self + @errors.concat @keys.map { |k| Validation.new(@params[k]).instance_exec(@params, &block).errors }.flatten + self end def as_array - @errors.concat @keys.reject{|k| @params[k].is_a?(Array)}.map{|k| "#{k} must be an array"} + @errors.concat @keys.reject { |k| @params[k].is_a?(Array) }.map { |k| "#{k} must be an array" } end def array_of(&block) - @errors.concat @keys.reject{|k| @params[k].is_a?(Array)}.map{|k| "#{k} must be an array"} - @errors.concat @keys.map{|k| @params[k].map{|h| Validation.new(h).instance_exec(@params, &block).errors}}.flatten - return self + @errors.concat @keys.reject { |k| @params[k].is_a?(Array) }.map { |k| "#{k} must be an array" } + @errors.concat @keys.map { |k| @params[k].map { |h| Validation.new(h).instance_exec(@params, &block).errors } }.flatten + self end def as_date with_format /\d\d\d\d-\d\d-\d\d/ - @errors.concat @keys.map{|k| [k].concat @params[k].split('-').map(&:to_i)} - .reject{|key, year, month, day| year.present? && year > 1000 && year < 3000 && month.present? && month > 0 && month < 13 && day.present? && day > 0 && day < 32} - .map{|k, _,_,_| "#{k} must be a valid date"} + @errors.concat @keys.map { |k| [k].concat @params[k].split('-').map(&:to_i) } + .reject { |_key, year, month, day| year.present? && year > 1000 && year < 3000 && month.present? && month > 0 && month < 13 && day.present? && day > 0 && day < 32 } + .map { |k, _, _, _| "#{k} must be a valid date" } end def min(n) - @errors.concat @keys.reject{|k| @params[k] >= n}.map{|k| "#{k} must be at least #{n}"} - return self + @errors.concat @keys.reject { |k| @params[k] >= n }.map { |k| "#{k} must be at least #{n}" } + self end def max(n) - @errors.concat @keys.reject{|k| @params[k] <= n}.map{|k| "#{k} must be less than #{n + 1}"} - return self + @errors.concat @keys.reject { |k| @params[k] <= n }.map { |k| "#{k} must be less than #{n + 1}" } + self end - # TODO min_len, max_len, as_float, as_currency, as_time, as_datetime + # TODO: min_len, max_len, as_float, as_currency, as_time, as_datetime # TODO return err resp on unrecognized params - end end diff --git a/lib/list/list_activities.rb b/lib/list/list_activities.rb index da740931..d2ea977b 100644 --- a/lib/list/list_activities.rb +++ b/lib/list/list_activities.rb @@ -1,17 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module ListActivities - - def self.with_params(params, default_activities=nil) - acts = default_activities || Activity - acts = acts.includes(:supporter, :attachment, :host).order('created_at DESC') - acts = acts.where(host_id: params[:host_id]) unless params[:host_id].blank? - acts = acts.where(attachment_id: params[:attachment_id]) unless params[:attachment_id].blank? - acts = acts.where(nonprofit_id: params[:nonprofit_id]) unless params[:nonprofit_id].blank? - if params[:public] - acts = acts.is_public - end - acts = acts.limit(params[:limit]) unless params[:limit].blank? - return acts - end - + def self.with_params(params, default_activities = nil) + acts = default_activities || Activity + acts = acts.includes(:supporter, :attachment, :host).order('created_at DESC') + acts = acts.where(host_id: params[:host_id]) unless params[:host_id].blank? + acts = acts.where(attachment_id: params[:attachment_id]) unless params[:attachment_id].blank? + acts = acts.where(nonprofit_id: params[:nonprofit_id]) unless params[:nonprofit_id].blank? + acts = acts.is_public if params[:public] + acts = acts.limit(params[:limit]) unless params[:limit].blank? + acts + end end diff --git a/lib/mailchimp.rb b/lib/mailchimp.rb index 31f06bc4..8738d35e 100644 --- a/lib/mailchimp.rb +++ b/lib/mailchimp.rb @@ -1,168 +1,164 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'httparty' require 'digest/md5' module Mailchimp - include HTTParty - format :json + include HTTParty + format :json def self.base_uri(key) dc = get_datacenter(key) - return "https://#{dc}.api.mailchimp.com/3.0" + "https://#{dc}.api.mailchimp.com/3.0" end - # Run the configuration from an initializer - # data: {:api_key => String} - def self.config(hash) - @options = { - :headers => { - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - } - } - @body = { - :apikey => hash[:api_key] - } - end + # Run the configuration from an initializer + # data: {:api_key => String} + def self.config(hash) + @options = { + headers: { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + } + } + @body = { + apikey: hash[:api_key] + } + end # Given a nonprofit mailchimp oauth2 key, return its current datacenter def self.get_datacenter(key) - metadata = HTTParty.get('https://login.mailchimp.com/oauth2/metadata', { - headers: { - 'User-Agent' => 'oauth2-draft-v10', - 'Host' => 'login.mailchimp.com', - 'Accept' => 'application/json', - 'Authorization' => "OAuth #{key}" - } - }) - return metadata['dc'] + metadata = HTTParty.get('https://login.mailchimp.com/oauth2/metadata', + headers: { + 'User-Agent' => 'oauth2-draft-v10', + 'Host' => 'login.mailchimp.com', + 'Accept' => 'application/json', + 'Authorization' => "OAuth #{key}" + }) + metadata['dc'] end - def self.signup email, mailchimp_list_id - body_hash = @body.merge({ - :id => mailchimp_list_id, - :email => {:email => email} - }) - post("/lists/subscribe", @options.merge(:body => body_hash.to_json)).parsed_response + def self.signup(email, mailchimp_list_id) + body_hash = @body.merge( + id: mailchimp_list_id, + email: { email: email } + ) + post('/lists/subscribe', @options.merge(body: body_hash.to_json)).parsed_response end def self.get_mailchimp_token(npo_id) mailchimp_token = QueryNonprofitKeys.get_key(npo_id, 'mailchimp_token') throw RuntimeError.new("No Mailchimp connection for this nonprofit: #{npo_id}") if mailchimp_token.nil? - return mailchimp_token + mailchimp_token end # Given a nonprofit id and a list of tag master ids that they make into email lists, # create those email lists on mailchimp and return an array of hashes of mailchimp list ids, names, and tag_master_id - def self.create_mailchimp_lists(npo_id, tag_master_ids) + def self.create_mailchimp_lists(npo_id, tag_master_ids) mailchimp_token = get_mailchimp_token(npo_id) uri = base_uri(mailchimp_token) puts "URI #{uri}" puts "KEY #{mailchimp_token}" npo = Qx.fetch(:nonprofits, npo_id).first - tags = Qx.select("DISTINCT(tag_masters.name) AS tag_name, tag_masters.id") - .from(:tag_masters) - .where({"tag_masters.nonprofit_id" => npo_id}) - .and_where("tag_masters.id IN ($ids)", ids: tag_master_ids) - .join(:nonprofits, "tag_masters.nonprofit_id = nonprofits.id") - .execute + tags = Qx.select('DISTINCT(tag_masters.name) AS tag_name, tag_masters.id') + .from(:tag_masters) + .where('tag_masters.nonprofit_id' => npo_id) + .and_where('tag_masters.id IN ($ids)', ids: tag_master_ids) + .join(:nonprofits, 'tag_masters.nonprofit_id = nonprofits.id') + .execute tags.map do |h| - list = post(uri+'/lists', { - basic_auth: {username: '', password: mailchimp_token}, - headers: {'Content-Type' => 'application/json'}, - body: { - name: 'CommitChange-'+h['tag_name'], - contact: { - company: npo['name'], - address1: npo['address'] || '', - city: npo['city'] || '', - state: npo['state_code'] || '', - zip: npo['zip_code'] || '', - country: npo['state_code'] || '', - phone: npo['phone'] || '' - }, - permission_reminder: 'You are a registered supporter of our nonprofit.', - campaign_defaults: { - from_name: npo['name'] || '', - from_email: npo['email'].blank? ? "support@commichange.com" : npo['email'], - subject: "Enter your subject here...", - language: 'en' - }, - email_type_option: false, - visibility: 'prv' - }.to_json - }) - if list.code != 200 - raise Exception.new("Failed to create list: #{list}") - end - {id: list['id'], name: list['name'], tag_master_id: h['id']} + list = post(uri + '/lists', + basic_auth: { username: '', password: mailchimp_token }, + headers: { 'Content-Type' => 'application/json' }, + body: { + name: 'CommitChange-' + h['tag_name'], + contact: { + company: npo['name'], + address1: npo['address'] || '', + city: npo['city'] || '', + state: npo['state_code'] || '', + zip: npo['zip_code'] || '', + country: npo['state_code'] || '', + phone: npo['phone'] || '' + }, + permission_reminder: 'You are a registered supporter of our nonprofit.', + campaign_defaults: { + from_name: npo['name'] || '', + from_email: npo['email'].blank? ? 'support@commichange.com' : npo['email'], + subject: 'Enter your subject here...', + language: 'en' + }, + email_type_option: false, + visibility: 'prv' + }.to_json) + raise Exception, "Failed to create list: #{list}" if list.code != 200 + + { id: list['id'], name: list['name'], tag_master_id: h['id'] } end end # Given a nonprofit id and post_data, which is an array of batch operation hashes # See here: http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-batch-operations/ - # Perform all the batch operations and return a status report + # Perform all the batch operations and return a status report def self.perform_batch_operations(npo_id, post_data) return if post_data.empty? + mailchimp_token = get_mailchimp_token(npo_id) uri = base_uri(mailchimp_token) - batch_job_id = post(uri + '/batches', { - basic_auth: {username: "CommitChange", password: mailchimp_token}, - headers: {'Content-Type' => 'application/json'}, - body: {operations: post_data}.to_json - })['id'] + batch_job_id = post(uri + '/batches', + basic_auth: { username: 'CommitChange', password: mailchimp_token }, + headers: { 'Content-Type' => 'application/json' }, + body: { operations: post_data }.to_json)['id'] check_batch_status(npo_id, batch_job_id) end def self.check_batch_status(npo_id, batch_job_id) mailchimp_token = get_mailchimp_token(npo_id) uri = base_uri(mailchimp_token) - batch_status = get(uri+'/batches/'+batch_job_id, { - basic_auth: {username: "CommitChange", password: mailchimp_token}, - headers: {'Content-Type' => 'application/json'} - }) + batch_status = get(uri + '/batches/' + batch_job_id, + basic_auth: { username: 'CommitChange', password: mailchimp_token }, + headers: { 'Content-Type' => 'application/json' }) end def self.delete_mailchimp_lists(npo_id, mailchimp_list_ids) mailchimp_token = get_mailchimp_token(npo_id) uri = base_uri(mailchimp_token) mailchimp_list_ids.map do |id| - delete(uri + "/lists/#{id}", {basic_auth: {username: "CommitChange", password: mailchimp_token}}) + delete(uri + "/lists/#{id}", basic_auth: { username: 'CommitChange', password: mailchimp_token }) end end # `removed` and `added` are arrays of tag join ids that have been added or removed to a supporter def self.sync_supporters_to_list_from_tag_joins(npo_id, supporter_ids, tag_data) - emails = Qx.select(:email).from(:supporters).where("id IN ($ids)", ids: supporter_ids).execute.map{|h| h['email']} - to_add = get_mailchimp_list_ids(tag_data.select{|h| h['selected']}.map{|h| h['tag_master_id']}) - to_remove = get_mailchimp_list_ids(tag_data.reject{|h| h['selected']}.map{|h| h['tag_master_id']}) + emails = Qx.select(:email).from(:supporters).where('id IN ($ids)', ids: supporter_ids).execute.map { |h| h['email'] } + to_add = get_mailchimp_list_ids(tag_data.select { |h| h['selected'] }.map { |h| h['tag_master_id'] }) + to_remove = get_mailchimp_list_ids(tag_data.reject { |h| h['selected'] }.map { |h| h['tag_master_id'] }) return if to_add.empty? && to_remove.empty? - bulk_post = emails.map{|em| to_add.map{|ml_id| {method: 'POST', path: "lists/#{ml_id}/members", body: {email_address: em, status: 'subscribed'}.to_json}}}.flatten - bulk_delete = emails.map{|em| to_remove.map{|ml_id| {method: 'DELETE', path: "lists/#{ml_id}/members/#{Digest::MD5.hexdigest(em.downcase).to_s}"}}}.flatten + bulk_post = emails.map { |em| to_add.map { |ml_id| { method: 'POST', path: "lists/#{ml_id}/members", body: { email_address: em, status: 'subscribed' }.to_json } } }.flatten + bulk_delete = emails.map { |em| to_remove.map { |ml_id| { method: 'DELETE', path: "lists/#{ml_id}/members/#{Digest::MD5.hexdigest(em.downcase)}" } } }.flatten perform_batch_operations(npo_id, bulk_post.concat(bulk_delete)) end def self.get_mailchimp_list_ids(tag_master_ids) return [] if tag_master_ids.empty? - to_insert_data = Qx.select("email_lists.mailchimp_list_id") - .from(:tag_masters) - .where("tag_masters.id IN ($ids)", ids: tag_master_ids) - .join("email_lists", "email_lists.tag_master_id=tag_masters.id") - .execute.map{|h| h['mailchimp_list_id']} - end + to_insert_data = Qx.select('email_lists.mailchimp_list_id') + .from(:tag_masters) + .where('tag_masters.id IN ($ids)', ids: tag_master_ids) + .join('email_lists', 'email_lists.tag_master_id=tag_masters.id') + .execute.map { |h| h['mailchimp_list_id'] } + end # @param [Nonprofit] nonprofit def self.hard_sync_lists(nonprofit) - return if !nonprofit + return unless nonprofit nonprofit.tag_masters.not_deleted.each do |i| - if (i.email_list) - hard_sync_list(i.email_list) - end + hard_sync_list(i.email_list) if i.email_list end end @@ -170,54 +166,52 @@ module Mailchimp def self.hard_sync_list(email_list) ops = generate_batch_ops_for_hard_sync(email_list) perform_batch_operations(email_list.nonprofit.id, ops) - - end + end def self.generate_batch_ops_for_hard_sync(email_list) - #get the subscribers from mailchimp + # get the subscribers from mailchimp mailchimp_subscribers = get_list_mailchimp_subscribers(email_list) - #get our subscribers - our_supporters = email_list.tag_master.tag_joins.map{|i| i.supporter} + # get our subscribers + our_supporters = email_list.tag_master.tag_joins.map(&:supporter) - #split them as follows: + # split them as follows: # on both lists, on our list, on the mailchimp list in_both, in_mailchimp_only = mailchimp_subscribers.partition do |mc_sub| - our_supporters.any?{|s| s.email.downcase == mc_sub[:email_address].downcase} + our_supporters.any? { |s| s.email.casecmp(mc_sub[:email_address]).zero? } end - + _, in_our_side_only = our_supporters.partition do |s| - mailchimp_subscribers.any?{|mc_sub| s.email.downcase == mc_sub[:email_address].downcase} + mailchimp_subscribers.any? { |mc_sub| s.email.casecmp(mc_sub[:email_address]).zero? } end # if on our list, add to mailchimp - output = in_our_side_only.map{|i| - {method: 'POST', path: "lists/#{email_list.mailchimp_list_id}/members", body: {email_address: i.email, status: 'subscribed'}.to_json} - } + output = in_our_side_only.map do |i| + { method: 'POST', path: "lists/#{email_list.mailchimp_list_id}/members", body: { email_address: i.email, status: 'subscribed' }.to_json } + end # if on mailchimp list, delete from mailchimp - output = output.concat(in_mailchimp_only.map{|i| {method: 'DELETE', path: "lists/#{email_list.mailchimp_list_id}/members/#{i[:id]}"}}) + output = output.concat(in_mailchimp_only.map { |i| { method: 'DELETE', path: "lists/#{email_list.mailchimp_list_id}/members/#{i[:id]}" } }) - return output + output end def self.get_list_mailchimp_subscribers(email_list) mailchimp_token = get_mailchimp_token(email_list.tag_master.nonprofit.id) uri = base_uri(mailchimp_token) - result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members?count=1000000000", { - basic_auth: {username: "CommitChange", password: mailchimp_token}, - headers: {'Content-Type' => 'application/json'}}) - members = result['members'].map do |i| - {id: i['id'], email_address: i['email_address']} - end.to_a + result = get(uri + "/lists/#{email_list.mailchimp_list_id}/members?count=1000000000", + basic_auth: { username: 'CommitChange', password: mailchimp_token }, + headers: { 'Content-Type' => 'application/json' }) + members = result['members'].map do |i| + { id: i['id'], email_address: i['email_address'] } + end.to_a end def self.get_email_lists(nonprofit) mailchimp_token = get_mailchimp_token(nonprofit.id) uri = base_uri(mailchimp_token) - result = get(uri + "/lists", { - basic_auth: {username: "CommitChange", password: mailchimp_token}, - headers: {'Content-Type' => 'application/json'}}) + result = get(uri + '/lists', + basic_auth: { username: 'CommitChange', password: mailchimp_token }, + headers: { 'Content-Type' => 'application/json' }) result['lists'] - - end + end end diff --git a/lib/maintain/maintain_dedications.rb b/lib/maintain/maintain_dedications.rb index d58ae687..d67f5099 100644 --- a/lib/maintain/maintain_dedications.rb +++ b/lib/maintain/maintain_dedications.rb @@ -1,44 +1,42 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module MaintainDedications def self.retrieve_json_dedications - return Qx.select('id', 'dedication').from(:donations) - .where("is_valid_json(dedication)").ex - + Qx.select('id', 'dedication').from(:donations) + .where('is_valid_json(dedication)').ex end - def self.retrieve_non_json_dedications(include_blank=false) + def self.retrieve_non_json_dedications(include_blank = false) temp = Qx.select('id', 'dedication').from(:donations) temp = temp.where("dedication IS NOT NULL AND dedication != ''") unless include_blank - temp = temp.and_where("NOT is_valid_json(dedication)") - return temp.ex + temp = temp.and_where('NOT is_valid_json(dedication)') + temp.ex end def self.create_json_dedications_from_plain_text(dedications) dedications.map do |i| - output = {id: i['id']} + output = { id: i['id'] } if i['dedication'] =~ /(((in (loving )?)?memory of|in memorium)\:? )(.+)/i - output[:dedication] = JSON.generate({type: 'memory', note: $+ }) + output[:dedication] = JSON.generate(type: 'memory', note: $+) elsif i['dedication'] =~ /((in honor of|honor of)\:? )(.+)/i - output[:dedication] = JSON.generate({type: 'honor', note: $+ }) + output[:dedication] = JSON.generate(type: 'honor', note: $+) else - output[:dedication] = JSON.generate({type: 'honor', note: i['dedication'] }) + output[:dedication] = JSON.generate(type: 'honor', note: i['dedication']) end output end.each do |i| - Qx.update(:donations).where('id = $id', {id: i[:id]}).set({dedication: i[:dedication]}).ex + Qx.update(:donations).where('id = $id', id: i[:id]).set(dedication: i[:dedication]).ex end end def self.add_honor_to_any_json_dedications_without_type(json_dedications) - json_dedications.map{|i| {'id' => i['id'], 'dedication' => JSON::parse(i['dedication']) }} - .select{|i| !%w(honor memory).include?(i['dedication']['type'])} - .map{|i| i['dedication']['type'] = 'honor'; i } - .each do |i| - Qx.update(:donations).where('id = $id', id: i['id']) - .set(dedication: JSON.generate(i['dedication'])).ex - end + json_dedications.map { |i| { 'id' => i['id'], 'dedication' => JSON.parse(i['dedication']) } } + .reject { |i| %w[honor memory].include?(i['dedication']['type']) } + .map { |i| i['dedication']['type'] = 'honor'; i } + .each do |i| + Qx.update(:donations).where('id = $id', id: i['id']) + .set(dedication: JSON.generate(i['dedication'])).ex + end end - - - -end \ No newline at end of file +end diff --git a/lib/maintain/maintain_payment_records.rb b/lib/maintain/maintain_payment_records.rb index b95edf92..a4705cc4 100644 --- a/lib/maintain/maintain_payment_records.rb +++ b/lib/maintain/maintain_payment_records.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module MaintainPaymentRecords # For records which have no associated charge, refund, nonprofit, supporter, donation or a gross_amount # The record is basically useless @@ -6,7 +8,7 @@ module MaintainPaymentRecords end def self.set_payment_supporter_and_nonprofit_though_charge_refund(i) - p = Payment.includes(:refund => :charge).find(i) + p = Payment.includes(refund: :charge).find(i) p.supporter_id = p.refund.charge.supporter_id p.nonprofit_id = p.refund.charge.nonprofit_id p.refund.disbursed = true @@ -16,9 +18,7 @@ module MaintainPaymentRecords def self.delete_payment_and_offsite_payment_record(id) p = Payment.includes(:offsite_payment).find(id) - if (p.offsite_payment) - p.offsite_payment.destroy - end + p.offsite_payment&.destroy p.destroy end -end \ No newline at end of file +end diff --git a/lib/maintain/maintain_ticket_records.rb b/lib/maintain/maintain_ticket_records.rb index d4035197..abfdb27d 100644 --- a/lib/maintain/maintain_ticket_records.rb +++ b/lib/maintain/maintain_ticket_records.rb @@ -1,18 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module MaintainTicketRecords - # a function for taking every ticket record with a card and creating a token # if the event was in the last two weeks def self.tokenize_cards_already_on_tickets Qx.transaction do - event_ids = Event.where('end_datetime >= ?', Time.current-2.weeks).pluck(:id) + event_ids = Event.where('end_datetime >= ?', Time.current - 2.weeks).pluck(:id) t = Ticket.includes(:card).includes(:event).where('card_id IS NOT NULL and event_id IN (?)', event_ids) - t.each{|i| - token = InsertSourceToken.create_record(i.card, {event: i.event}) + t.each do |i| + token = InsertSourceToken.create_record(i.card, event: i.event) i.source_token = token i.save! - } + end end end -end \ No newline at end of file +end diff --git a/lib/merge_supporters.rb b/lib/merge_supporters.rb index a679b30b..04a79035 100644 --- a/lib/merge_supporters.rb +++ b/lib/merge_supporters.rb @@ -1,81 +1,80 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later - module MergeSupporters +# frozen_string_literal: true +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module MergeSupporters # For supporters that have been merged, we want to update all their child tables to the new supporter_id def self.update_associations(old_supporter_ids, new_supporter_id, np_id, profile_id) # The new supporter needs to have the following tables from the merged supporters: - associations = [:activities, :donations, :recurring_donations, :offsite_payments, :payments, :tickets, :supporter_notes, :supporter_emails, :full_contact_infos] - + associations = %i[activities donations recurring_donations offsite_payments payments tickets supporter_notes supporter_emails full_contact_infos] + associations.each do |table_name| - Qx.update(table_name).set(supporter_id: new_supporter_id).where("supporter_id IN ($ids)", ids: old_supporter_ids).timestamps.execute + Qx.update(table_name).set(supporter_id: new_supporter_id).where('supporter_id IN ($ids)', ids: old_supporter_ids).timestamps.execute end old_supporters = Supporter.includes(:tag_joins).includes(:custom_field_joins).where('id in (?)', old_supporter_ids) - old_tags = old_supporters.map{|i| i.tag_joins.map{|j| j.tag_master}}.flatten.uniq + old_tags = old_supporters.map { |i| i.tag_joins.map(&:tag_master) }.flatten.uniq - #delete old tags + # delete old tags InsertTagJoins.in_bulk(np_id, profile_id, old_supporter_ids, - old_tags.map{|i| {tag_master_id: i.id, selected: false}}) + old_tags.map { |i| { tag_master_id: i.id, selected: false } }) + InsertTagJoins.in_bulk(np_id, profile_id, [new_supporter_id], old_tags.map { |i| { tag_master_id: i.id, selected: true } }) - InsertTagJoins.in_bulk(np_id, profile_id, [new_supporter_id], old_tags.map{|i| {tag_master_id: i.id, selected: true}}) + all_custom_field_joins = old_supporters.map(&:custom_field_joins).flatten + group_joins_by_custom_field_master = all_custom_field_joins.group_by { |i| i.custom_field_master.id } + one_custom_field_join_per_user = group_joins_by_custom_field_master.map do |_k, v| + v.sort_by(&:created_at).reverse.first + end - all_custom_field_joins = old_supporters.map{| i| i.custom_field_joins}.flatten - group_joins_by_custom_field_master = all_custom_field_joins.group_by{|i| i.custom_field_master.id} - one_custom_field_join_per_user = group_joins_by_custom_field_master.map{|k,v| - v.sort_by{|i| - i.created_at - }.reverse.first} + # delete old supporter custom_field + InsertCustomFieldJoins.in_bulk(np_id, old_supporter_ids, one_custom_field_join_per_user.map do |i| + { + custom_field_master_id: i.custom_field_master_id, + value: '' + } + end) - #delete old supporter custom_field - InsertCustomFieldJoins.in_bulk(np_id, old_supporter_ids, one_custom_field_join_per_user.map{|i| { - custom_field_master_id: i.custom_field_master_id, - value: "" - }}) - - #insert new supporter custom field - InsertCustomFieldJoins.in_bulk(np_id, [new_supporter_id], one_custom_field_join_per_user.map{|i| { - custom_field_master_id: i.custom_field_master_id, - value: i.value - }}) + # insert new supporter custom field + InsertCustomFieldJoins.in_bulk(np_id, [new_supporter_id], one_custom_field_join_per_user.map do |i| + { + custom_field_master_id: i.custom_field_master_id, + value: i.value + } + end) # Update all deleted/merged supporters to record when and where they got merged - Psql.execute(Qexpr.new.update(:supporters, {merged_at: Time.current, merged_into: new_supporter_id}).where("id IN ($ids)", ids: old_supporter_ids)) + Psql.execute(Qexpr.new.update(:supporters, merged_at: Time.current, merged_into: new_supporter_id).where('id IN ($ids)', ids: old_supporter_ids)) # Removing any duplicate custom fields UpdateCustomFieldJoins.delete_dupes([new_supporter_id]) end - def self.selected(merged_data, supporter_ids,np_id, profile_id) + def self.selected(merged_data, supporter_ids, np_id, profile_id) new_supporter = Supporter.new(merged_data) new_supporter.save! # Update merged supporters as deleted - Psql.execute(Qexpr.new.update(:supporters, {deleted: true}).where("id IN ($ids)", ids: supporter_ids)) + Psql.execute(Qexpr.new.update(:supporters, deleted: true).where('id IN ($ids)', ids: supporter_ids)) # Update all associated tables - self.update_associations(supporter_ids, new_supporter['id'],np_id, profile_id) - return {json: new_supporter, status: :ok} + update_associations(supporter_ids, new_supporter['id'], np_id, profile_id) + { json: new_supporter, status: :ok } end - # Merge supporters for a nonprofit based on an array of groups of ids, generated from QuerySupporters.dupes_on_email or dupes_on_names def self.merge_by_id_groups(np_id, arr_of_ids, profile_id) Qx.transaction do - arr_of_ids.select{|arr| arr.count > 1}.each do |ids| + arr_of_ids.select { |arr| arr.count > 1 }.each do |ids| # Get all column data from every supporter all_data = Psql.execute( Qexpr.new.from(:supporters) .select(:email, :name, :phone, :address, :city, :state_code, :zip_code, :organization, :country, :created_at) - .where("id IN ($ids)", ids: ids) - .order_by("created_at ASC") + .where('id IN ($ids)', ids: ids) + .order_by('created_at ASC') ) # Use the most recent non null/blank column data for the new supporter - data = all_data.reduce({}) do |acc, supp| - supp.except('created_at').each{|key, val| acc[key] = val unless val.blank?} - acc - end.merge({'nonprofit_id' => np_id}) + data = all_data.each_with_object({}) do |supp, acc| + supp.except('created_at').each { |key, val| acc[key] = val unless val.blank? } + end.merge('nonprofit_id' => np_id) MergeSupporters.selected(data, ids, np_id, profile_id) end end end - - end diff --git a/lib/metrics/nonprofit_metrics.rb b/lib/metrics/nonprofit_metrics.rb index 73aed549..ee651236 100644 --- a/lib/metrics/nonprofit_metrics.rb +++ b/lib/metrics/nonprofit_metrics.rb @@ -1,110 +1,110 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module NonprofitMetrics - def self.payments(np_id) Qx.select( - "(SUM(payments.gross_amount) / 100.0)::money::text AS total", - "(AVG(payments.gross_amount) / 100.0)::money::text AS average", - "(SUM(week.gross_amount) / 100.0)::money::text AS week", - "(SUM(month.gross_amount) / 100.0)::money::text AS month", - "(SUM(year.gross_amount) / 100.0)::money::text AS year", + '(SUM(payments.gross_amount) / 100.0)::money::text AS total', + '(AVG(payments.gross_amount) / 100.0)::money::text AS average', + '(SUM(week.gross_amount) / 100.0)::money::text AS week', + '(SUM(month.gross_amount) / 100.0)::money::text AS month', + '(SUM(year.gross_amount) / 100.0)::money::text AS year' ) - .from(:payments) - .left_join( - ['payments week', "week.id=payments.id AND week.date > date_trunc('week', NOW())"], - ['payments month', "month.id=payments.id AND month.date > date_trunc('month', NOW())"], - ['payments year', "year.id=payments.id AND year.date > date_trunc('year', NOW())"] - ) - .where("payments.nonprofit_id=$id", id: np_id) - .execute.last + .from(:payments) + .left_join( + ['payments week', "week.id=payments.id AND week.date > date_trunc('week', NOW())"], + ['payments month', "month.id=payments.id AND month.date > date_trunc('month', NOW())"], + ['payments year', "year.id=payments.id AND year.date > date_trunc('year', NOW())"] + ) + .where('payments.nonprofit_id=$id', id: np_id) + .execute.last end def self.recurring(np_id) # total, average, this month Qx.select( - "(SUM(recurring_donations.amount) / 100.0)::money::text AS total", - "(AVG(recurring_donations.amount) / 100.0)::money::text AS average", - "(SUM(month.amount) / 100.0)::money::text AS month", + '(SUM(recurring_donations.amount) / 100.0)::money::text AS total', + '(AVG(recurring_donations.amount) / 100.0)::money::text AS average', + '(SUM(month.amount) / 100.0)::money::text AS month' ) - .from(:recurring_donations) - .left_join("recurring_donations month", "month.id=recurring_donations.id AND month.created_at > date_trunc('month', NOW())") - .where("recurring_donations.active=TRUE") - .and_where("recurring_donations.n_failures < 3") - .and_where("recurring_donations.nonprofit_id=$id", id: np_id) - .execute.last + .from(:recurring_donations) + .left_join('recurring_donations month', "month.id=recurring_donations.id AND month.created_at > date_trunc('month', NOW())") + .where('recurring_donations.active=TRUE') + .and_where('recurring_donations.n_failures < 3') + .and_where('recurring_donations.nonprofit_id=$id', id: np_id) + .execute.last end def self.supporters(np_id) Qx.select( - "COUNT(supporters) AS total", - "COUNT(week) AS week", - "COUNT(month) AS month" + 'COUNT(supporters) AS total', + 'COUNT(week) AS week', + 'COUNT(month) AS month' ) - .from(:supporters) - .left_join("supporters week", "week.id=supporters.id AND week.created_at > date_trunc('week', NOW()) AND week.imported_at IS NULL") - .add_left_join("supporters month", "month.id=supporters.id AND month.created_at > date_trunc('month', NOW()) AND month.imported_at IS NULL") - .where("coalesce(supporters.deleted, FALSE) = FALSE") - .and_where("supporters.nonprofit_id=$id", id: np_id) - .execute.last + .from(:supporters) + .left_join('supporters week', "week.id=supporters.id AND week.created_at > date_trunc('week', NOW()) AND week.imported_at IS NULL") + .add_left_join('supporters month', "month.id=supporters.id AND month.created_at > date_trunc('month', NOW()) AND month.imported_at IS NULL") + .where('coalesce(supporters.deleted, FALSE) = FALSE') + .and_where('supporters.nonprofit_id=$id', id: np_id) + .execute.last end def self.recent_donations(np_id) Qx.select( - "(payments.gross_amount / 100)::money::text AS amount", - "payments.date", - "payments.id AS payment_id", - "supporters.name AS supporter_name", - "supporters.email AS supporter_email" + '(payments.gross_amount / 100)::money::text AS amount', + 'payments.date', + 'payments.id AS payment_id', + 'supporters.name AS supporter_name', + 'supporters.email AS supporter_email' ) - .from(:payments) - .join("supporters", "payments.supporter_id=supporters.id") - .where("payments.nonprofit_id=$id", id: np_id) - .and_where("payments.kind IN ('Donation', 'RecurringDonation', 'Ticket')") - .limit(10) - .order_by("payments.date DESC") - .execute + .from(:payments) + .join('supporters', 'payments.supporter_id=supporters.id') + .where('payments.nonprofit_id=$id', id: np_id) + .and_where("payments.kind IN ('Donation', 'RecurringDonation', 'Ticket')") + .limit(10) + .order_by('payments.date DESC') + .execute end def self.recent_supporters(np_id) - Qx.select("name", "email", "id", "created_at") - .from(:supporters) - .where("supporters.nonprofit_id=$id", id: np_id) - .and_where("coalesce(supporters.deleted, FALSE) = FALSE") - .and_where("supporters.import_id IS NULL") - .limit(10) - .order_by("supporters.created_at DESC") - .execute + Qx.select('name', 'email', 'id', 'created_at') + .from(:supporters) + .where('supporters.nonprofit_id=$id', id: np_id) + .and_where('coalesce(supporters.deleted, FALSE) = FALSE') + .and_where('supporters.import_id IS NULL') + .limit(10) + .order_by('supporters.created_at DESC') + .execute end def self.all_metrics(np_id) - keys = [:payments, :recurring, :supporters, :recent_donations, :recent_supporters, :published_campaigns] - keys.reduce({}) do |accum, elem| + keys = %i[payments recurring supporters recent_donations recent_supporters published_campaigns] + keys.each_with_object({}) do |elem, accum| accum[elem] = NonprofitMetrics.send(elem, np_id) - accum end end def self.published_campaigns(np_id) Qx.select( - "campaigns.name", - "campaigns.id", - "campaigns.created_at", - "campaigns.end_datetime", - "COUNT(supporters.id) AS supporter_count", - "(SUM(one_time.amount)/ 100)::money::text AS total_one_time", - "(SUM(recurring_donations.amount)/ 100)::money::text AS total_recurring" + 'campaigns.name', + 'campaigns.id', + 'campaigns.created_at', + 'campaigns.end_datetime', + 'COUNT(supporters.id) AS supporter_count', + '(SUM(one_time.amount)/ 100)::money::text AS total_one_time', + '(SUM(recurring_donations.amount)/ 100)::money::text AS total_recurring' ) - .from(:campaigns) - .left_join("donations", "donations.campaign_id=campaigns.id") - .add_left_join("donations AS one_time", "donations.id=one_time.id AND one_time.recurring_donation_id IS NULL") - .add_left_join("recurring_donations", "recurring_donations.donation_id=donations.id AND recurring_donations.active=TRUE") - .add_left_join("supporters", "supporters.id=donations.supporter_id") - .group_by("campaigns.id") - .where("campaigns.nonprofit_id=$id", id: np_id) - .and_where("campaigns.published = TRUE") - .order_by("campaigns.end_datetime DESC") - .execute + .from(:campaigns) + .left_join('donations', 'donations.campaign_id=campaigns.id') + .add_left_join('donations AS one_time', 'donations.id=one_time.id AND one_time.recurring_donation_id IS NULL') + .add_left_join('recurring_donations', 'recurring_donations.donation_id=donations.id AND recurring_donations.active=TRUE') + .add_left_join('supporters', 'supporters.id=donations.supporter_id') + .group_by('campaigns.id') + .where('campaigns.nonprofit_id=$id', id: np_id) + .and_where('campaigns.published = TRUE') + .order_by('campaigns.end_datetime DESC') + .execute end # Given a starting date, ending date, and time interval, @@ -113,44 +113,44 @@ module NonprofitMetrics # each hash is nested in an outer hash, set to a key that is also the date, lol # this is used in the payment_history query to fill in missing dates in the data. def self.payment_history_timespans(params) - raise ArgumentError.new("Invalid timespan") unless ['year', 'month', 'week', 'day'].include? params[:timeSpan] + raise ArgumentError, 'Invalid timespan' unless %w[year month week day].include? params[:timeSpan] + date_hash = {} beginning_of = 'beginning_of_' + params[:timeSpan] current_date = Chronic.parse(params[:startDate]).send(beginning_of) end_date = Chronic.parse(params[:endDate]).send(beginning_of) while current_date <= end_date - date = current_date.strftime("%F") - date_hash[date] = {'time_span' => date} + date = current_date.strftime('%F') + date_hash[date] = { 'time_span' => date } current_date += 1.send(params[:timeSpan]) end - return date_hash + date_hash end def self.payment_history(params) results = Qx.select( "to_char(date_trunc('#{params[:timeSpan]}', MAX(payments.date)), 'YYYY-MM-DD') AS time_span", - "coalesce(SUM(payments.gross_amount), 0) AS total_cents", - "coalesce(SUM(onetime.gross_amount ), 0) AS onetime_cents", - "coalesce(SUM(recurring.gross_amount ), 0) AS recurring_cents", - "coalesce(SUM(tickets.gross_amount ), 0) AS tickets_cents" + 'coalesce(SUM(payments.gross_amount), 0) AS total_cents', + 'coalesce(SUM(onetime.gross_amount ), 0) AS onetime_cents', + 'coalesce(SUM(recurring.gross_amount ), 0) AS recurring_cents', + 'coalesce(SUM(tickets.gross_amount ), 0) AS tickets_cents' ) - .from(:payments) - .left_join( - ["payments AS onetime", "onetime.id=payments.id AND onetime.kind='Donation'"], - ["payments AS recurring", "recurring.id=payments.id AND recurring.kind='RecurringDonation'"], - ["payments AS tickets", "tickets.id=payments.id AND tickets.kind='Ticket'"] - ) - .where("payments.nonprofit_id" => params[:id]) - .and_where("payments.date >= $d", d: params[:startDate]) - .and_where("payments.date <= $d", d: params[:endDate]) - .group_by("date_trunc('#{params[:timeSpan]}', payments.date)") - .order_by("MAX(payments.date)") - .execute + .from(:payments) + .left_join( + ['payments AS onetime', "onetime.id=payments.id AND onetime.kind='Donation'"], + ['payments AS recurring', "recurring.id=payments.id AND recurring.kind='RecurringDonation'"], + ['payments AS tickets', "tickets.id=payments.id AND tickets.kind='Ticket'"] + ) + .where('payments.nonprofit_id' => params[:id]) + .and_where('payments.date >= $d', d: params[:startDate]) + .and_where('payments.date <= $d', d: params[:endDate]) + .group_by("date_trunc('#{params[:timeSpan]}', payments.date)") + .order_by('MAX(payments.date)') + .execute date_hash = payment_history_timespans(params) - return results.reduce(date_hash){|acc, r| acc[r['time_span']] = r; acc}.values + results.each_with_object(date_hash) { |r, acc| acc[r['time_span']] = r; }.values end - end diff --git a/lib/name_copy_naming_algorithm.rb b/lib/name_copy_naming_algorithm.rb index 1fba6644..f70aae5f 100644 --- a/lib/name_copy_naming_algorithm.rb +++ b/lib/name_copy_naming_algorithm.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NameCopyNamingAlgorithm < CopyNamingAlgorithm - attr_accessor :klass, :nonprofit_id # @param [Class] klass def initialize(klass, nonprofit_id) - @klass = klass - @nonprofit_id = nonprofit_id + @klass = klass + @nonprofit_id = nonprofit_id end def copy_addition @@ -13,7 +14,7 @@ class NameCopyNamingAlgorithm < CopyNamingAlgorithm end def separator_before_copy_number - " " + ' ' end def max_copies @@ -29,13 +30,10 @@ class NameCopyNamingAlgorithm < CopyNamingAlgorithm end def get_already_used_name_entities(base_name) - end_name = "#{copy_addition.gsub("(","\\(").gsub(")", "\\)")} \\d{2}" + end_name = "#{copy_addition.gsub('(', '\\(').gsub(')', '\\)')} \\d{2}" end_name_length = copy_addition.length + 3 amount_to_strip = end_name_length + base_name.length - max_length - if (amount_to_strip < 0) - amount_to_strip = 0 - end - @klass.method(:where).call('name SIMILAR TO ? AND nonprofit_id = ? AND (deleted IS NULL OR deleted = false)', "#{base_name[0..base_name.length-amount_to_strip-1]}_*" + end_name, nonprofit_id).select('name') + amount_to_strip = 0 if amount_to_strip < 0 + @klass.method(:where).call('name SIMILAR TO ? AND nonprofit_id = ? AND (deleted IS NULL OR deleted = false)', "#{base_name[0..base_name.length - amount_to_strip - 1]}_*" + end_name, nonprofit_id).select('name') end - -end \ No newline at end of file +end diff --git a/lib/notify/notify_user.rb b/lib/notify/notify_user.rb index 1a1073f1..ca0f85f7 100644 --- a/lib/notify/notify_user.rb +++ b/lib/notify/notify_user.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module NotifyUser - def self.send_confirmation_email(user_id) - ParamValidation.new({user_id: user_id}, user_id: {required:true, is_integer: true}) + ParamValidation.new({ user_id: user_id }, user_id: { required: true, is_integer: true }) user = User.where('id = ?', user_id).first - if !user - raise ParamValidation::ValidationError.new("#{user_id} is not a valid user id", {key: :user_id, val: user_id}) + unless user + raise ParamValidation::ValidationError.new("#{user_id} is not a valid user id", key: :user_id, val: user_id) end user.send_confirmation_instructions end -end \ No newline at end of file +end diff --git a/lib/numeric.rb b/lib/numeric.rb index af0b53b8..bf657fc0 100644 --- a/lib/numeric.rb +++ b/lib/numeric.rb @@ -1,18 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Numeric # Works like Numeric#floor but uses an offset other than 1. Ex: 6.floor_for_delta(5) -> 5 # @param [Integer] delta the integer offsets from zero to round down to # @return [Integer] def floor_for_delta(delta) - raise ArgumentError.new('delta must be a positive integer') unless delta.is_a?(Integer) && delta > 0 - (self % delta).zero? ? self : ((self.to_i / delta)) * delta; + raise ArgumentError, 'delta must be a positive integer' unless delta.is_a?(Integer) && delta > 0 + + (self % delta).zero? ? self : ((to_i / delta)) * delta end # Works like Numeric#ceil but uses an offset other than 1. Ex: 6.floor_for_delta(5) -> 10 # @param [Integer] delta the integer offsets from zero to round up to # @return [Integer] def ceil_for_delta(delta) - raise ArgumentError.new('delta must be a positive integer') unless delta.is_a?(Integer) && delta > 0 - (self % delta).zero? ? self : ((self.floor.to_i / delta)+1) * delta; + raise ArgumentError, 'delta must be a positive integer' unless delta.is_a?(Integer) && delta > 0 + + (self % delta).zero? ? self : ((floor.to_i / delta) + 1) * delta end -end \ No newline at end of file +end diff --git a/lib/onboard_accounts.rb b/lib/onboard_accounts.rb index a04a605f..38e944c4 100644 --- a/lib/onboard_accounts.rb +++ b/lib/onboard_accounts.rb @@ -1,51 +1,50 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'param_validation' require 'qx' module OnboardAccounts - def self.create_org(params) nonprofit_data = set_nonprofit_defaults(params['nonprofit']) - ParamValidation.new(nonprofit_data, { - name: {required: true}, - # email: {required: true}, - # phone: {required: true}, - city: {required: true}, - state_code: {required: true} - }) + ParamValidation.new(nonprofit_data, + name: { required: true }, + # email: {required: true}, + # phone: {required: true}, + city: { required: true }, + state_code: { required: true }) user_data = set_user_defaults(params['user']) - ParamValidation.new(user_data, { - name: {required: true}, - email: {required: true}, - password: {required: true}, - phone: {required: true} - }) + ParamValidation.new(user_data, + name: { required: true }, + email: { required: true }, + password: { required: true }, + phone: { required: true }) extra_info = params['extraInfo'] nonprofit = Qx.insert_into(:nonprofits) - .values(nonprofit_data).timestamps - .returning('*') - .execute.last + .values(nonprofit_data).timestamps + .returning('*') + .execute.last billing_plan_id = Settings.default_bp.id billing_subscription = Qx.insert_into(:billing_subscriptions) - .values({ - nonprofit_id: nonprofit['id'], - billing_plan_id: billing_plan_id, - status: 'active' - }) - .timestamps.execute.last + .values( + nonprofit_id: nonprofit['id'], + billing_plan_id: billing_plan_id, + status: 'active' + ) + .timestamps.execute.last # Create the user using the User and Role models (since we have to use Devise) user = User.create!(user_data) role = Qx.insert_into(:roles) - .values(user_id: user.id, name: 'nonprofit_admin', host_id: nonprofit['id'], host_type: 'Nonprofit') - .timestamps - .execute.last + .values(user_id: user.id, name: 'nonprofit_admin', host_id: nonprofit['id'], host_type: 'Nonprofit') + .timestamps + .execute.last - self.delay.send_onboard_email(nonprofit, nonprofit_data, user_data, extra_info) + delay.send_onboard_email(nonprofit, nonprofit_data, user_data, extra_info) - return { + { nonprofit: nonprofit, user: user, role: role, @@ -54,59 +53,57 @@ module OnboardAccounts end ### ethis is a one time method in order to add a user without testing for the method. Do not use this long term - def self.create_org_with_user(params, user=nil) + def self.create_org_with_user(params, user = nil) nonprofit_data = set_nonprofit_defaults(params['nonprofit']) - ParamValidation.new(nonprofit_data, { - name: {required: true}, - # email: {required: true}, - # phone: {required: true}, - city: {required: true}, - state_code: {required: true} - }) - if (!user) + ParamValidation.new(nonprofit_data, + name: { required: true }, + # email: {required: true}, + # phone: {required: true}, + city: { required: true }, + state_code: { required: true }) + unless user user_data = set_user_defaults(params['user']) - ParamValidation.new(user_data, { - name: {required: true}, - email: {required: true}, - password: {required: true}, - phone: {required: true} - }) + ParamValidation.new(user_data, + name: { required: true }, + email: { required: true }, + password: { required: true }, + phone: { required: true }) end extra_info = params['extraInfo'] nonprofit = Qx.insert_into(:nonprofits) - .values(nonprofit_data).timestamps - .returning('*') - .execute.last + .values(nonprofit_data).timestamps + .returning('*') + .execute.last # Create a billing subscription for the 6% fee tier billing_plan_id = Settings.default_bp.id billing_subscription = Qx.insert_into(:billing_subscriptions) - .values({ - nonprofit_id: nonprofit['id'], - billing_plan_id: billing_plan_id, - status: 'active' - }) - .timestamps.execute.last + .values( + nonprofit_id: nonprofit['id'], + billing_plan_id: billing_plan_id, + status: 'active' + ) + .timestamps.execute.last # Create the user using the User and Role models (since we have to use Devise) user = !user ? User.create!(user_data) : user role = Qx.insert_into(:roles) - .values(user_id: user.id, name: 'nonprofit_admin', host_id: nonprofit['id'], host_type: 'Nonprofit') - .timestamps - .execute.last + .values(user_id: user.id, name: 'nonprofit_admin', host_id: nonprofit['id'], host_type: 'Nonprofit') + .timestamps + .execute.last - self.delay.send_onboard_email(nonprofit, nonprofit_data, user_data, extra_info) + delay.send_onboard_email(nonprofit, nonprofit_data, user_data, extra_info) - return { - nonprofit: nonprofit, - user: user, - role: role, - billing_subscription: billing_subscription + { + nonprofit: nonprofit, + user: user, + role: role, + billing_subscription: billing_subscription } end def self.set_nonprofit_defaults(data) - data = data.merge({ + data = data.merge( verification_status: 'unverified', published: true, vetted: Settings.nonprofits_must_be_vetted ? false : true, @@ -114,7 +111,7 @@ module OnboardAccounts city_slug: Format::Url.convert_to_slug(data[:city]), state_code_slug: Format::Url.convert_to_slug(data[:state_code]), slug: Format::Url.convert_to_slug(data[:name]) - }) + ) data end @@ -130,7 +127,7 @@ module OnboardAccounts NonprofitMailer.welcome(np['id']).deliver # Send an email notifying people internal folks of the new nonporfit, with the above info and extra_info to_emails = ['support@commitchange.com'] - message = %Q( + message = %( New signup on CommitChange for an organization with the name "#{np['name']}" Location: #{np['city']} #{np['state_code']}, #{np['zip_code']} Org Email: #{nonprofit_data['email']} @@ -140,7 +137,7 @@ module OnboardAccounts User Phone: #{user_data['phone']} Entity Type: #{extra_info['entity_type']} How they heard about us: #{extra_info['how_they_heard']} - What they want to use: #{['use_donations', 'use_crm', 'use_campaigns', 'use_events'].select{|x| extra_info[x] == 'on'}.join(", ")} + What they want to use: #{%w[use_donations use_crm use_campaigns use_events].select { |x| extra_info[x] == 'on' }.join(', ')} ) subject = "New Account Signup: #{np['name']}" GenericMailer.generic_mail('support@commitchange.com', 'CC Bot', message, subject, to_emails, '').deliver diff --git a/lib/parallel_ar.rb b/lib/parallel_ar.rb index 40d5629e..9537381f 100644 --- a/lib/parallel_ar.rb +++ b/lib/parallel_ar.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'parallel' module ParallelAr - def self.reduce(arr, accum, &block) Parallel.each(arr, in_threads: 8) do |elem| ActiveRecord::Base.connection_pool.with_connection do @@ -11,7 +12,6 @@ module ParallelAr end end end - return accum + accum end - end diff --git a/lib/path/nonprofit_path.rb b/lib/path/nonprofit_path.rb index 3b4cb912..f7a1de48 100644 --- a/lib/path/nonprofit_path.rb +++ b/lib/path/nonprofit_path.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module NonprofitPath + def self.show(np) + return '/' unless np - def self.show(np) - return "/" unless np - "/#{np.state_code_slug}/#{np.city_slug}/#{np.slug}" - end + "/#{np.state_code_slug}/#{np.city_slug}/#{np.slug}" + end - def self.dashboard(np) - "#{show(np)}/dashboard" - end + def self.dashboard(np) + "#{show(np)}/dashboard" + end end diff --git a/lib/pay_recurring_donation.rb b/lib/pay_recurring_donation.rb index 200158c9..f8f798cb 100644 --- a/lib/pay_recurring_donation.rb +++ b/lib/pay_recurring_donation.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'insert/insert_donation' require 'insert/insert_supporter_notes' @@ -5,8 +7,6 @@ require 'timespan' require 'delayed_job_helper' module PayRecurringDonation - - # Pay ALL recurring donations that are currently due; each payment gets a queued delayed_job # Returns the number of queued jobs def self.pay_all_due_with_stripe @@ -14,24 +14,23 @@ module PayRecurringDonation ids = Psql.execute_vectors( QueryRecurringDonations._all_that_are_due )[1..-1].flatten - + jobs = ids.map do |id| - {handler: DelayedJobHelper.create_handler(PayRecurringDonation, :with_stripe, [id])} + { handler: DelayedJobHelper.create_handler(PayRecurringDonation, :with_stripe, [id]) } end - Psql.execute(Qexpr.new.insert(:delayed_jobs, jobs, { - common_data: { - run_at: Time.current, - attempts: 0, - failed_at: nil, - last_error: nil, - locked_at: nil, - locked_by: nil, - priority: 0, - queue: "rec-don-payments" - } - })) - return ids + Psql.execute(Qexpr.new.insert(:delayed_jobs, jobs, + common_data: { + run_at: Time.current, + attempts: 0, + failed_at: nil, + last_error: nil, + locked_at: nil, + locked_by: nil, + priority: 0, + queue: 'rec-don-payments' + })) + ids end # run the payrecurring_donation in development so I can make sure we have the expected failures @@ -60,20 +59,19 @@ module PayRecurringDonation # Charge an existing donation via stripe, only if it is due # Pass in an instance of an existing RecurringDonation def self.with_stripe(rd_id) - ParamValidation.new({:rd_id => rd_id}, { - :rd_id => { - :required => true, - :is_integer=> true - } - }) + ParamValidation.new({ rd_id: rd_id }, + rd_id: { + required: true, + is_integer: true + }) rd = RecurringDonation.where('id = ?', rd_id).first unless rd - raise ParamValidation::ValidationError.new("#{rd_id} is not a valid recurring donation", {:key => :rd_id}) + raise ParamValidation::ValidationError.new("#{rd_id} is not a valid recurring donation", key: :rd_id) end - return false if !QueryRecurringDonations.is_due?(rd_id) + return false unless QueryRecurringDonations.is_due?(rd_id) donation = Donation.where('id = ?', rd['donation_id']).first unless donation @@ -81,94 +79,93 @@ module PayRecurringDonation end result = {} - result = result.merge(InsertDonation.insert_charge({ - 'card_id' => donation['card_id'], - 'recurring_donation' => true, - 'designation' => donation['designation'], - 'amount' => donation['amount'], - 'nonprofit_id' => donation['nonprofit_id'], - 'donation_id' => donation['id'], - 'supporter_id' => donation['supporter_id'], - 'old_donation' => true - })) + result = result.merge(InsertDonation.insert_charge( + 'card_id' => donation['card_id'], + 'recurring_donation' => true, + 'designation' => donation['designation'], + 'amount' => donation['amount'], + 'nonprofit_id' => donation['nonprofit_id'], + 'donation_id' => donation['id'], + 'supporter_id' => donation['supporter_id'], + 'old_donation' => true + )) if result['charge']['status'] != 'failed' result['recurring_donation'] = Psql.execute( - Qexpr.new.update(:recurring_donations, {n_failures: 0}) - .where("id=$id", id: rd_id).returning('*') + Qexpr.new.update(:recurring_donations, n_failures: 0) + .where('id=$id', id: rd_id).returning('*') ).first Delayed::Job.enqueue JobTypes::DonorPaymentNotificationJob.new(rd['donation_id']) Delayed::Job.enqueue JobTypes::NonprofitPaymentNotificationJob.new(rd['donation_id']) InsertActivities.for_recurring_donations([result['payment']['id']]) else result['recurring_donation'] = Psql.execute( - Qexpr.new.update(:recurring_donations, {n_failures: rd['n_failures'] + 1}) - .where("id=$id", id: rd_id).returning('*') + Qexpr.new.update(:recurring_donations, n_failures: rd['n_failures'] + 1) + .where('id=$id', id: rd_id).returning('*') ).first DonationMailer.delay.donor_failed_recurring_donation(rd['donation_id']) if rd['n_failures'] >= 3 DonationMailer.delay.nonprofit_failed_recurring_donation(rd['donation_id']) end - InsertSupporterNotes.create([{content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", supporter_id: donation['supporter_id'], user_id: 540}]) + InsertSupporterNotes.create([{ content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", supporter_id: donation['supporter_id'], user_id: 540 }]) end - return result + result end - def self.fail_a_recurring_donation(rd, donation, notify_nonprofit=false) + def self.fail_a_recurring_donation(rd, donation, notify_nonprofit = false) recurring_donation = Psql.execute( - Qexpr.new.update(:recurring_donations, {n_failures: 3}) - .where("id=$id", id: rd['id']).returning('*') + Qexpr.new.update(:recurring_donations, n_failures: 3) + .where('id=$id', id: rd['id']).returning('*') ).first DonationMailer.delay.donor_failed_recurring_donation(rd['donation_id']) if notify_nonprofit DonationMailer.delay.nonprofit_failed_recurring_donation(rd['donation_id']) end - InsertSupporterNotes.create([{content: "This supporter had a payment failure for their recurring donation with ID #{rd['id']}", supporter_id: donation['supporter_id'], user_id: 540}]) - return recurring_donation + InsertSupporterNotes.create([{ content: "This supporter had a payment failure for their recurring donation with ID #{rd['id']}", supporter_id: donation['supporter_id'], user_id: 540 }]) + recurring_donation end # Charge an existing donation via stripe, NO MATTER WHAT # Pass in an instance of an existing RecurringDonation - def self.with_stripe_BUT_NO_MATTER_WHAT(rd_id, enter_todays_date, run_this=false, set_this_true=false, this_one_needs_to_be_false=true, is_this_run_dangerously="no") - - if (PayRecurringDonation::ULTIMATE_VERIFICATION(enter_todays_date, run_this, set_this_true, this_one_needs_to_be_false, is_this_run_dangerously)) + def self.with_stripe_BUT_NO_MATTER_WHAT(rd_id, enter_todays_date, run_this = false, set_this_true = false, this_one_needs_to_be_false = true, is_this_run_dangerously = 'no') + if PayRecurringDonation::ULTIMATE_VERIFICATION(enter_todays_date, run_this, set_this_true, this_one_needs_to_be_false, is_this_run_dangerously) rd = Psql.execute("SELECT * FROM recurring_donations WHERE id=#{rd_id}").first donation = Psql.execute("SELECT * FROM donations WHERE id=#{rd['donation_id']}").first result = {} - result = result.merge(InsertDonation.insert_charge({ - 'card_id' => donation['card_id'], - 'recurring_donation' => true, - 'designation' => donation['designation'], - 'amount' => donation['amount'], - 'nonprofit_id' => donation['nonprofit_id'], - 'donation_id' => donation['id'], - 'supporter_id' => donation['supporter_id'] - })) + result = result.merge(InsertDonation.insert_charge( + 'card_id' => donation['card_id'], + 'recurring_donation' => true, + 'designation' => donation['designation'], + 'amount' => donation['amount'], + 'nonprofit_id' => donation['nonprofit_id'], + 'donation_id' => donation['id'], + 'supporter_id' => donation['supporter_id'] + )) if result['charge']['status'] != 'failed' result['recurring_donation'] = Psql.execute( - Qexpr.new.update(:recurring_donations, {n_failures: 0}) - .where("id=$id", id: rd_id).returning('*') + Qexpr.new.update(:recurring_donations, n_failures: 0) + .where('id=$id', id: rd_id).returning('*') ).first Delayed::Job.enqueue JobTypes::DonorPaymentNotificationJob.new(rd['donation_id']) Delayed::Job.enqueue JobTypes::NonprofitPaymentNotificationJob.new(rd['donation_id']) InsertActivities.for_recurring_donations([result['payment']['id']]) else result['recurring_donation'] = Psql.execute( - Qexpr.new.update(:recurring_donations, {n_failures: rd['n_failures'] + 1}) - .where("id=$id", id: rd_id).returning('*') + Qexpr.new.update(:recurring_donations, n_failures: rd['n_failures'] + 1) + .where('id=$id', id: rd_id).returning('*') ).first DonationMailer.delay.donor_failed_recurring_donation(rd['donation_id']) if rd['n_failures'] >= 3 DonationMailer.delay.nonprofit_failed_recurring_donation(rd['donation_id']) end - InsertSupporterNotes.create([{content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", supporter_id: donation['supporter_id'], user_id: 540}]) + InsertSupporterNotes.create([{ content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", supporter_id: donation['supporter_id'], user_id: 540 }]) end return result end - return false + false end - def self.ULTIMATE_VERIFICATION(enter_todays_date, run_this=false, set_this_true=false, this_one_needs_to_be_false=true, is_this_run_dangerously="no") - return (Date.parse(enter_todays_date) == Date.today() && run_this && set_this_true && !this_one_needs_to_be_false && is_this_run_dangerously == "run dangerously") + def self.ULTIMATE_VERIFICATION(enter_todays_date, run_this = false, set_this_true = false, this_one_needs_to_be_false = true, is_this_run_dangerously = 'no') + (Date.parse(enter_todays_date) == Date.today && run_this && set_this_true && !this_one_needs_to_be_false && is_this_run_dangerously == 'run dangerously') end end diff --git a/lib/psql.rb b/lib/psql.rb index a9a18a1a..4226ce88 100644 --- a/lib/psql.rb +++ b/lib/psql.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Some convenience wrappers around the postgresql gem, allowing us to avoid activerecord dependency # combine usage of this library with Qexpr @@ -9,11 +11,10 @@ require 'qx' # Initialize the database connection module Psql - # Execute a sql statement (string) def self.execute(statement) puts statement if ENV['RAILS_ENV'] != 'production' && ENV['RAILS_LOG_LEVEL'] == 'debug' # log to STDOUT on dev/staging - return Qx.execute_raw(raw_expr_str(statement)) + Qx.execute_raw(raw_expr_str(statement)) end # A variation of execute that returns a vector of vectors rather than a vector of hashes @@ -21,7 +22,7 @@ module Psql def self.execute_vectors(statement) puts statement if ENV['RAILS_ENV'] != 'production' && ENV['RAILS_LOG_LEVEL'] == 'debug' # log to STDOUT on dev/staging raw_str = statement.to_s.uncolorize.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') - return Qx.execute_raw(raw_expr_str(statement), format: 'csv') + Qx.execute_raw(raw_expr_str(statement), format: 'csv') end def self.transaction(&block) @@ -30,11 +31,10 @@ module Psql end end -private + private # Raw expression string def self.raw_expr_str(statement) statement.to_s.uncolorize.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '') end - end diff --git a/lib/qexpr.rb b/lib/qexpr.rb index e0948527..fa3bee63 100644 --- a/lib/qexpr.rb +++ b/lib/qexpr.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # A module that allows you to construct complex SQL expressions by piecing # together methods in ruby. @@ -12,22 +14,19 @@ require 'hamster' require 'colorize' class Qexpr - attr_accessor :tree - - def initialize(h=nil) + def initialize(h = nil) @tree = Hamster::Hash[h] end - def to_s - self.parse + parse end # Parse an qexpr object into a sql string expression def parse - expr = "" + expr = '' if @tree[:insert] expr = "#{@tree[:insert]} #{@tree[:values].blue}" expr += "\nRETURNING ".bold.light_blue + (@tree[:returning] || ['id']).join(', ').blue @@ -37,8 +36,9 @@ class Qexpr # Query-based expessions expr = @tree[:update] || @tree[:delete_from] || @tree[:select] if expr.nil? || expr.empty? - raise ArgumentError.new("Must have a select, update, or delete clause") + raise ArgumentError, 'Must have a select, update, or delete clause' end + if @tree[:from] expr += "\nFROM".bold.light_blue + @tree[:from].map do |f| f.is_a?(String) ? f : " (#{f[:sub_expr].parse}\n) AS #{f[:as]}" @@ -52,44 +52,39 @@ class Qexpr expr += @tree[:limit] if @tree[:limit] expr += @tree[:offset] if @tree[:offset] - if @tree[:select] && @tree[:as] - expr = "(#{expr}) AS #{@tree[:as]}" - end + expr = "(#{expr}) AS #{@tree[:as]}" if @tree[:select] && @tree[:as] if @tree[:update] && @tree[:returning] expr += "\nRETURNING ".bold.light_blue + @tree[:returning].join(', ').blue end - return expr + expr end - # insert into table_name the values from every hash inside of arr # optionally pass in: # no_timestamps: don't set created_at and updated_at # common_data: a hash of data to set for all rows # returning: what columns to return - def insert(table_name, arr, options={}) + def insert(table_name, arr, options = {}) arr = [arr] if arr.is_a? Hash - arr = arr.map{|h| h.sort.to_h} # Make sure all key/vals are ordered the same way + arr = arr.map { |h| h.sort.to_h } # Make sure all key/vals are ordered the same way keys = arr.first.keys keys = keys.concat(options[:common_data].keys) if options[:common_data] - keys = keys.map{|k| "\"#{k}\""}.join(', ') - ts_columns = options[:no_timestamps] ? "" : "created_at, updated_at, " - ts_values = options[:no_timestamps] ? "" : "#{Qexpr.now}, #{Qexpr.now}, " - common_vals = options[:common_data] ? options[:common_data].values.map{|v| Qexpr.quote(v)} : [] - vals = arr.map{|h| '(' + ts_values + h.values.map{|v| Qexpr.quote(v)}.concat(common_vals).join(',') + ')'}.join(',') + keys = keys.map { |k| "\"#{k}\"" }.join(', ') + ts_columns = options[:no_timestamps] ? '' : 'created_at, updated_at, ' + ts_values = options[:no_timestamps] ? '' : "#{Qexpr.now}, #{Qexpr.now}, " + common_vals = options[:common_data] ? options[:common_data].values.map { |v| Qexpr.quote(v) } : [] + vals = arr.map { |h| '(' + ts_values + h.values.map { |v| Qexpr.quote(v) }.concat(common_vals).join(',') + ')' }.join(',') Qexpr.new @tree - .put(:insert, "INSERT INTO".bold.light_blue + " #{table_name} (#{ts_columns} #{keys})".blue) + .put(:insert, 'INSERT INTO'.bold.light_blue + " #{table_name} (#{ts_columns} #{keys})".blue) .put(:values, "\nVALUES".bold.light_blue + " #{vals}".blue) end - - def update(table_name, settings, os={}) - Qexpr.new @tree.put(:update, "UPDATE".bold.light_blue + " #{table_name}".blue + "\nSET".bold.light_blue + " #{settings.map{|key,val| "#{key.to_s}=#{Qexpr.quote(val)}"}.join(', ')}".blue) + def update(table_name, settings, _os = {}) + Qexpr.new @tree.put(:update, 'UPDATE'.bold.light_blue + " #{table_name}".blue + "\nSET".bold.light_blue + " #{settings.map { |key, val| "#{key}=#{Qexpr.quote(val)}" }.join(', ')}".blue) end - def delete_from(table_name) - Qexpr.new @tree.put(:delete_from, "DELETE FROM".bold.light_blue + " #{table_name}".blue) + Qexpr.new @tree.put(:delete_from, 'DELETE FROM'.bold.light_blue + " #{table_name}".blue) end # Create or append select columns @@ -97,12 +92,12 @@ class Qexpr if @tree[:select] Qexpr.new @tree.put(:select, @tree[:select] + ", #{cols.join(', ')}".blue) else - if cols.count < 4 - cols = " #{cols.join(", ")}" - else - cols = "\n #{cols.join("\n, ")}" - end - Qexpr.new @tree.put(:select, "\nSELECT".bold.light_blue + "#{cols}".blue) + cols = if cols.count < 4 + " #{cols.join(', ')}" + else + "\n #{cols.join("\n, ")}" + end + Qexpr.new @tree.put(:select, "\nSELECT".bold.light_blue + cols.to_s.blue) end end @@ -110,67 +105,59 @@ class Qexpr Qexpr.new @tree.put(:select, "\nSELECT DISTINCT".bold.light_blue + "\n #{cols.join("\n, ")}".blue) end - def select_distinct_on(cols_distinct, cols_select) - Qexpr.new @tree.put(:select, "SELECT DISTINCT ON".bold.light_blue + " (#{Array(cols_distinct).join(', ')})\n #{Array(cols_select).join("\n, ")}".blue) + Qexpr.new @tree.put(:select, 'SELECT DISTINCT ON'.bold.light_blue + " (#{Array(cols_distinct).join(', ')})\n #{Array(cols_select).join("\n, ")}".blue) end - - def from(expr, as=nil) + def from(expr, as = nil) Qexpr.new @tree.put(:from, (@tree[:from] || Hamster::Vector[]).add(Qexpr.from_expr(expr, as))) end - def group_by(*cols) Qexpr.new @tree.put(:group_by, "\nGROUP BY".bold.light_blue + " #{cols.join(', ')}".blue) end - def order_by(expr) - Qexpr.new @tree.put(:order_by, "\nORDER BY".bold.light_blue + " #{expr.to_s}".blue) + Qexpr.new @tree.put(:order_by, "\nORDER BY".bold.light_blue + " #{expr}".blue) end - def limit(i) Qexpr.new @tree.put(:limit, "\nLIMIT".bold.light_blue + " #{i.to_i}".blue) end - def offset(i) Qexpr.new @tree.put(:offset, "\nOFFSET".bold.light_blue + " #{i.to_i}".blue) end - - def join(table_name, on_expr, data={}) + def join(table_name, on_expr, data = {}) on_expr = Qexpr.interpolate_expr(on_expr, data) - return Qexpr.new @tree - .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nJOIN".bold.light_blue + " #{table_name}\n ".blue + "ON".bold.light_blue + " #{on_expr}".blue)) + Qexpr.new @tree + .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nJOIN".bold.light_blue + " #{table_name}\n ".blue + 'ON'.bold.light_blue + " #{on_expr}".blue)) end - def inner_join(table_name, on_expr, data={}) + def inner_join(table_name, on_expr, data = {}) on_expr = Qexpr.interpolate_expr(on_expr, data) - return Qexpr.new @tree - .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nINNER JOIN".bold.light_blue + " #{table_name}\n ".blue + "ON".bold.light_blue + " #{on_expr}".blue)) + Qexpr.new @tree + .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nINNER JOIN".bold.light_blue + " #{table_name}\n ".blue + 'ON'.bold.light_blue + " #{on_expr}".blue)) end - def left_outer_join(table_name, on_expr, data={}) + def left_outer_join(table_name, on_expr, data = {}) on_expr = Qexpr.interpolate_expr(on_expr, data) - return Qexpr.new @tree - .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nLEFT OUTER JOIN".bold.light_blue + " #{table_name}\n ".blue + "ON".bold.light_blue + " #{on_expr}".blue)) + Qexpr.new @tree + .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\nLEFT OUTER JOIN".bold.light_blue + " #{table_name}\n ".blue + 'ON'.bold.light_blue + " #{on_expr}".blue)) end - def join_lateral(join_name, select_statement, success_condition=true, data={}) + def join_lateral(join_name, select_statement, success_condition = true, data = {}) select_statement = Qexpr.interpolate_expr(select_statement, data) - return Qexpr.new @tree - .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\n JOIN LATERAL".bold.light_blue + " (#{select_statement})\n #{join_name} ".blue + "ON".bold.light_blue + " #{success_condition}".blue)) + Qexpr.new @tree + .put(:joins, (@tree[:joins] || Hamster::Vector[]).add("\n JOIN LATERAL".bold.light_blue + " (#{select_statement})\n #{join_name} ".blue + 'ON'.bold.light_blue + " #{success_condition}".blue)) end - def as(name) - return Qexpr.new @tree.put(:as, name) + Qexpr.new @tree.put(:as, name) end - def where(expr, data={}) + def where(expr, data = {}) expr = Qexpr.interpolate_expr(expr, data) if @tree[:where] Qexpr.new @tree.put(:where, @tree[:where] + "\nAND".bold.light_blue + " (#{expr})".blue) @@ -179,13 +166,11 @@ class Qexpr end end - def returning(*cols) Qexpr.new @tree.put(:returning, (@tree[:returning] || Hamster::Vector[]).concat(cols)) end - - def having(expr, data={}) + def having(expr, data = {}) if @tree[:having] Qexpr.new @tree.put(:having, @tree[:having] + "\nAND".bold.light_blue + " (#{Qexpr.interpolate_expr(expr, data)})".blue) else @@ -201,7 +186,7 @@ class Qexpr # Remove clauses from the expression # eg expr.remove(:from, :where) def remove(*keys) - return Qexpr.new keys.reduce(@tree){|tree, key| tree.delete(key)} + Qexpr.new keys.reduce(@tree) { |tree, key| tree.delete(key) } end # Quote a string for use in sql to prevent injection or weird errors @@ -211,19 +196,19 @@ class Qexpr def self.quote(val) if val.is_a?(Integer) || (val.is_a?(String) && val =~ /^\$Q\$.+\$Q\$$/) # is a valid num or already quoted val - elsif val == nil - "NULL" + elsif val.nil? + 'NULL' elsif !!val == val # is a boolean val ? "'t'" : "'f'" else - return "$Q$" + val.to_s + "$Q$" + '$Q$' + val.to_s + '$Q$' end end # An alias of PG.quote_ident, for convenience sake # Double-quotes sql identifiers def self.quote_ident(str) - str.split('.').map{|s| "\"#{s}\""}.join('.') + str.split('.').map { |s| "\"#{s}\"" }.join('.') end # sql-quoted datetime value useful for created_at and updated_at columns @@ -234,7 +219,7 @@ class Qexpr # Given a max page length and the current page, # return the offset value # (eg: page_length=30 and page=3, then return 60) - def self.page_offset(page_length, page=1) + def self.page_offset(page_length, page = 1) page = page.to_i page = 1 if page <= 0 Qexpr.quote((page.to_i - 1) * page_length.to_i) @@ -242,11 +227,12 @@ class Qexpr # Given the total row count, the max page length, and the current page, # return the total results left - def self.remaining_count(total_count, page_length, current_page=1) - return 0 unless current_page - rem = total_count.to_i - (current_page.to_i) * page_length.to_i - rem = 0 if rem < 0 - return rem + def self.remaining_count(total_count, page_length, current_page = 1) + return 0 unless current_page + + rem = total_count.to_i - current_page.to_i * page_length.to_i + rem = 0 if rem < 0 + rem end # Given a string sql expression with interpolations like "WHERE id > ${id}" @@ -256,14 +242,14 @@ class Qexpr expr.gsub(/\$\w+/) do |match| val = data[match.gsub(/[ \$]*/, '').to_sym] if val.is_a?(Array) || val.is_a?(Hamster::Vector) - val.to_a.map{|x| Qexpr.quote(x)}.join(', ') + val.to_a.map { |x| Qexpr.quote(x) }.join(', ') else Qexpr.quote val end end end -private + private # Given some kind of expr object (might be just a string or another whole Qexpr expr), and an 'as' value # then give back either a hash for the sub-Qexpr expression, or just a string. @@ -272,7 +258,7 @@ private if expr.is_a?(Qexpr) Hamster::Hash[sub_expr: expr, as: as] else - " #{expr} #{as ? "AS #{as.to_s}" : ""}" + " #{expr} #{as ? "AS #{as}" : ''}" end end end diff --git a/lib/qexpr_query_chunker.rb b/lib/qexpr_query_chunker.rb index c61cf28b..8853b25f 100644 --- a/lib/qexpr_query_chunker.rb +++ b/lib/qexpr_query_chunker.rb @@ -1,34 +1,27 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -#TODO combine these two items +# TODO combine these two items module QexprQueryChunker - # Used to get a chunk of a Qexpr query # @param [Integer] offset the offset for the beginning of the chunk # @param [Integer] limit the maximum number of rows to get in the chunk # @param [Boolean] skip_header whether you should skip the header row in the returned output. Defaults to false # @yieldreturn [Qexpr] a block which, when called, returns the main Qexpr query # @return [Enumerator] an Enumerator, with each item an array for a row - def self.get_chunk_of_query(offset=nil, limit=nil, skip_header=false, &block) - Enumerator.new {|y| + def self.get_chunk_of_query(offset = nil, limit = nil, skip_header = false, &block) + Enumerator.new do |y| + expr = block.call + expr = expr.offset(offset) if offset - expr = block.call() - if offset - expr = expr.offset(offset) - end - - if limit - expr = expr.limit(limit) - end + expr = expr.limit(limit) if limit vecs = Psql.execute_vectors(expr.parse) - if (!skip_header) - y << vecs.first.to_a.map{|k| k.to_s.titleize} + y << vecs.first.to_a.map { |k| k.to_s.titleize } unless skip_header - end - - vecs.drop(1).each{|v| y << v.to_a} - } + vecs.drop(1).each { |v| y << v.to_a } + end end # Get a lazy enumerable getting a query in chunks. block is a block used for creating a query for a new chunk @@ -39,23 +32,23 @@ module QexprQueryChunker # @yieldparam [Boolean] skip_header whether you should skip the header row in the returned output. # @yieldreturn [Enumerator] an Enumerator, with each item an array for a row # @return [Enumerator::Lazy] a lazy enumerator for getting every item in the query - def self.for_export_enumerable(chunk_limit=35000, &block) + def self.for_export_enumerable(chunk_limit = 35_000, &block) Enumerator.new do |y| last_export_length = 0 limit = chunk_limit page = 0 - while page == 0 || last_export_length == limit do + while page == 0 || last_export_length == limit # either we haven't started yet or the last export == limit (since if it didn't we're to the end) page += 1 offset = Qexpr.page_offset(limit, page) export_returned = block.call(offset, limit, page > 1).to_a - #we got the titles too if on_first, let's skip one row - last_export_length = page == 1 ? export_returned.length-1 : export_returned.length + # we got the titles too if on_first, let's skip one row + last_export_length = page == 1 ? export_returned.length - 1 : export_returned.length # efficient? no. Do we care? eh. - export_returned.each {|i| + export_returned.each do |i| y << i - } + end end end.lazy end -end \ No newline at end of file +end diff --git a/lib/query/billing_plans.rb b/lib/query/billing_plans.rb index c9973059..72ffa2ca 100644 --- a/lib/query/billing_plans.rb +++ b/lib/query/billing_plans.rb @@ -1,22 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module BillingPlans - def self.get_percentage_fee(nonprofit_id) - ParamValidation.new({nonprofit_id:nonprofit_id}, {nonprofit_id: {:required => true, :is_integer => true}}) + ParamValidation.new({ nonprofit_id: nonprofit_id }, nonprofit_id: { required: true, is_integer: true }) - unless (Nonprofit.exists?(nonprofit_id)) - raise ParamValidation::ValidationError.new("#{nonprofit_id} does not exist", {:key => :nonprofit_id} ) + unless Nonprofit.exists?(nonprofit_id) + raise ParamValidation::ValidationError.new("#{nonprofit_id} does not exist", key: :nonprofit_id) end - - result = Qx.select("billing_plans.percentage_fee") - .from("billing_plans") - .join("billing_subscriptions bs", "bs.billing_plan_id = billing_plans.id") - .where("bs.nonprofit_id=$id", id: nonprofit_id) - .execute - return result.empty? ? 0 : result.last['percentage_fee'] + result = Qx.select('billing_plans.percentage_fee') + .from('billing_plans') + .join('billing_subscriptions bs', 'bs.billing_plan_id = billing_plans.id') + .where('bs.nonprofit_id=$id', id: nonprofit_id) + .execute + result.empty? ? 0 : result.last['percentage_fee'] end - end diff --git a/lib/query/query_activities.rb b/lib/query/query_activities.rb index d1915b60..994edf3d 100644 --- a/lib/query/query_activities.rb +++ b/lib/query/query_activities.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module QueryActivities def self.for_timeline(nonprofit_id, supporter_id) - Qx.select( "activities.*") + Qx.select('activities.*') .from(:activities) .where("activities.supporter_id = #{supporter_id.to_i} AND activities.nonprofit_id = #{nonprofit_id.to_i}") .order_by('activities.date DESC') .execute end end - diff --git a/lib/query/query_billing_subscriptions.rb b/lib/query/query_billing_subscriptions.rb index 9962fc12..434b7b57 100644 --- a/lib/query/query_billing_subscriptions.rb +++ b/lib/query/query_billing_subscriptions.rb @@ -1,24 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' require 'active_support/core_ext' module QueryBillingSubscriptions - def self.days_left_in_trial(np_id) - sub = Qx.fetch(:billing_subscriptions, {nonprofit_id: np_id}).last + sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last return 0 if sub.nil? - return sub['status'] == 'trialing' ? (((sub['created_at'] + 10.days) - Time.current) / 86400).floor : 0 + + sub['status'] == 'trialing' ? (((sub['created_at'] + 10.days) - Time.current) / 86_400).floor : 0 end def self.plan_tier(np_id) - sub = Qx.fetch(:billing_subscriptions, {nonprofit_id: np_id}).last + sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last return 2 if sub && sub['status'] != 'inactive' - return 0 + + 0 end def self.currently_in_trial?(np_id) - sub = Qx.fetch(:billing_subscriptions, {nonprofit_id: np_id}).last - return sub && sub['status'] == 'trialing' + sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last + sub && sub['status'] == 'trialing' end end - diff --git a/lib/query/query_campaign_gifts.rb b/lib/query/query_campaign_gifts.rb index 6ebf2325..f4be0075 100644 --- a/lib/query/query_campaign_gifts.rb +++ b/lib/query/query_campaign_gifts.rb @@ -1,31 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Query code for both campaign_gift_options and campaign_gifts require 'psql' module QueryCampaignGifts - - - # Create a mapping of: { - # 'total_donations' => Integer, # total donations for gift options - # 'total_one_time' => Integer, # total one-time donations for gift option - # 'total_recurring' => Integer, - # 'name' => String, # name of the gift level - # - # Includes the overall sum as well as the donations without gift options - def self.report_metrics(campaign_id) - - - data = Psql.execute(%Q( + # Create a mapping of: { + # 'total_donations' => Integer, # total donations for gift options + # 'total_one_time' => Integer, # total one-time donations for gift option + # 'total_recurring' => Integer, + # 'name' => String, # name of the gift level + # + # Includes the overall sum as well as the donations without gift options + def self.report_metrics(campaign_id) + data = Psql.execute(%( SELECT campaign_gift_options.name , COUNT(*) AS total_donations , SUM(ds_one_time.amount) AS total_one_time , SUM(ds_recurring.amount) AS total_recurring FROM (#{donations_for_campaign(campaign_id).parse}) AS ds - LEFT OUTER JOIN (#{get_corresponding_payments(campaign_id, %Q(LEFT OUTER JOIN recurring_donations ON recurring_donations.donation_id = donations.id - ), %Q(WHERE recurring_donations.id IS NULL))}) ds_one_time + LEFT OUTER JOIN (#{get_corresponding_payments(campaign_id, %(LEFT OUTER JOIN recurring_donations ON recurring_donations.donation_id = donations.id + ), %(WHERE recurring_donations.id IS NULL))}) ds_one_time ON ds_one_time.id = ds.id - LEFT OUTER JOIN (#{get_corresponding_payments(campaign_id, %Q(INNER JOIN recurring_donations ON recurring_donations.donation_id = donations.id))}) ds_recurring + LEFT OUTER JOIN (#{get_corresponding_payments(campaign_id, %(INNER JOIN recurring_donations ON recurring_donations.donation_id = donations.id))}) ds_recurring ON ds_recurring.id = ds.id LEFT OUTER JOIN campaign_gifts ON campaign_gifts.donation_id=ds.id @@ -35,16 +33,15 @@ module QueryCampaignGifts ORDER BY total_donations DESC )) - return Hamster::Hash[data: data] - end + Hamster::Hash[data: data] + end - def self.donations_for_campaign(campaign_id) - Qx.select('donations.id, donations.amount').from(:donations).where("campaign_id IN ($ids)", {ids:QueryCampaigns.get_campaign_and_children(campaign_id) - }) - end + def self.donations_for_campaign(campaign_id) + Qx.select('donations.id, donations.amount').from(:donations).where('campaign_id IN ($ids)', ids: QueryCampaigns.get_campaign_and_children(campaign_id)) + end - def self.get_corresponding_payments(campaign_id, recurring_clauses, where_clauses="") - %Q(SELECT donations.id, payments.gross_amount AS amount + def self.get_corresponding_payments(campaign_id, recurring_clauses, where_clauses = '') + %(SELECT donations.id, payments.gross_amount AS amount FROM (#{donations_for_campaign(campaign_id).parse}) donations #{recurring_clauses} JOIN LATERAL ( @@ -55,6 +52,5 @@ module QueryCampaignGifts ) payments ON true #{where_clauses} ) - end + end end - diff --git a/lib/query/query_campaign_metrics.rb b/lib/query/query_campaign_metrics.rb index b2cc1529..30b03588 100644 --- a/lib/query/query_campaign_metrics.rb +++ b/lib/query/query_campaign_metrics.rb @@ -1,31 +1,29 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryCampaignMetrics - def self.on_donations(campaign_id) campaign = Campaign.find(campaign_id) result = Psql.execute( - Qexpr.new.select("COALESCE(COUNT(DISTINCT donations.id), 0) AS supporters_count", - "COALESCE(SUM(payments.gross_amount), 0) AS total_raised") - .from("campaigns") + Qexpr.new.select('COALESCE(COUNT(DISTINCT donations.id), 0) AS supporters_count', + 'COALESCE(SUM(payments.gross_amount), 0) AS total_raised') + .from('campaigns') .join( - "donations", "donations.campaign_id=campaigns.id" + 'donations', 'donations.campaign_id=campaigns.id' ) - .join_lateral("payments", QueryDonations.get_first_payment_for_donation.parse, true) + .join_lateral('payments', QueryDonations.get_first_payment_for_donation.parse, true) .where("campaigns.id IN (#{QueryCampaigns .get_campaign_and_children(campaign_id) - .parse - })") + .parse})") ).last - return { - 'supporters_count' => result['supporters_count'], - 'total_raised'=> result['total_raised'], - 'goal_amount'=> campaign.goal_amount, - 'show_total_count'=> campaign.show_total_count, - 'show_total_raised'=> campaign.show_total_raised + { + 'supporters_count' => result['supporters_count'], + 'total_raised' => result['total_raised'], + 'goal_amount' => campaign.goal_amount, + 'show_total_count' => campaign.show_total_count, + 'show_total_raised' => campaign.show_total_raised } end end - - diff --git a/lib/query/query_campaigns.rb b/lib/query/query_campaigns.rb index ec4eec89..b61775b6 100644 --- a/lib/query/query_campaigns.rb +++ b/lib/query/query_campaigns.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' module QueryCampaigns - def self.featured(limit, gross_more_than) expr = Qexpr.new.select( 'campaigns.name', @@ -11,77 +12,72 @@ module QueryCampaigns Image._url('campaigns', 'main_image', 'normal') + 'AS main_image_url', "concat('/nonprofits/', campaigns.nonprofit_id, '/campaigns/', campaigns.id) AS url" ).from( - Qexpr.new.select("campaigns.*", "SUM(donations.amount) AS gross") - .from("campaigns") - .join("donations", "donations.campaign_id=campaigns.id") - .group_by("campaigns.id"), - "campaigns" + Qexpr.new.select('campaigns.*', 'SUM(donations.amount) AS gross') + .from('campaigns') + .join('donations', 'donations.campaign_id=campaigns.id') + .group_by('campaigns.id'), + 'campaigns' ).where("campaigns.gross > $amount AND campaigns.published='t' AND campaigns.nonprofit_id!=$id", amount: gross_more_than, id: 4182) - .order_by("campaigns.updated_at ASC") - .limit(limit) + .order_by('campaigns.updated_at ASC') + .limit(limit) Psql.execute(expr.parse) end - def self.timeline(campaign_id) - ex = QueryCampaigns.payments_expression(campaign_id, true) - ex.group_by("DATE(payments.date)") - .order_by("DATE(payments.date)") + ex = QueryCampaigns.payments_expression(campaign_id, true) + ex.group_by('DATE(payments.date)') + .order_by('DATE(payments.date)') .execute end - def self.payments_expression(campaign_id, for_timeline) selects = [ - "coalesce(SUM(payments.gross_amount), 0) AS total_cents", - "coalesce(SUM(recurring.gross_amount), 0) AS recurring_cents", - "coalesce(SUM(offsite.gross_amount), 0) AS offsite_cents", - "coalesce(SUM(onetime.gross_amount), 0) AS onetime_cents"] + 'coalesce(SUM(payments.gross_amount), 0) AS total_cents', + 'coalesce(SUM(recurring.gross_amount), 0) AS recurring_cents', + 'coalesce(SUM(offsite.gross_amount), 0) AS offsite_cents', + 'coalesce(SUM(onetime.gross_amount), 0) AS onetime_cents' + ] - for_timeline ? - selects.push("MAX(DATE(payments.date)) AS date") : - selects.push("coalesce(count(supporters.id), 0) AS supporters_count") + for_timeline ? + selects.push('MAX(DATE(payments.date)) AS date') : + selects.push('coalesce(count(supporters.id), 0) AS supporters_count') - return Qx.select(*selects) - .from("payments") + Qx.select(*selects) + .from('payments') .left_join( - ["donations", "payments.donation_id=donations.id"], - ["payments AS onetime", "onetime.id=payments.id AND onetime.kind='Donation'"], - ["payments AS offsite", "offsite.id=payments.id AND offsite.kind='OffsitePayment'"], - ["payments AS recurring", "recurring.id=payments.id AND recurring.kind='RecurringDonation'"]) + ['donations', 'payments.donation_id=donations.id'], + ['payments AS onetime', "onetime.id=payments.id AND onetime.kind='Donation'"], + ['payments AS offsite', "offsite.id=payments.id AND offsite.kind='OffsitePayment'"], + ['payments AS recurring', "recurring.id=payments.id AND recurring.kind='RecurringDonation'"] + ) .where("donations.campaign_id IN (#{QueryCampaigns.get_campaign_and_children(campaign_id).parse})") end - def self.totals(campaign_id) - ex = QueryCampaigns.payments_expression(campaign_id, false) - ex.add_left_join(["supporters", "donations.supporter_id=supporters.id"]) + ex = QueryCampaigns.payments_expression(campaign_id, false) + ex.add_left_join(['supporters', 'donations.supporter_id=supporters.id']) .execute.first end - def self.name_and_id(npo_id) - np = Nonprofit.find(npo_id) campaigns = np.campaigns.not_deleted.includes(:profile).order('campaigns.name ASC') output = campaigns.map do |i| { - 'name' => i.name, - 'id' => i.id, - 'isChildCampaign' => i.child_campaign?, - 'creator' => i.profile&.name || "user ##{i.profile.id}" + 'name' => i.name, + 'id' => i.id, + 'isChildCampaign' => i.child_campaign?, + 'creator' => i.profile&.name || "user ##{i.profile.id}" } end output end def self.get_campaign_and_children(campaign_id) - Qx.select("id") - .from('campaigns') - .where("campaigns.id = $id OR campaigns.parent_campaign_id=$id", - id: campaign_id) + Qx.select('id') + .from('campaigns') + .where('campaigns.id = $id OR campaigns.parent_campaign_id=$id', + id: campaign_id) end - - end diff --git a/lib/query/query_charges.rb b/lib/query/query_charges.rb index 0f6c6019..832188db 100644 --- a/lib/query/query_charges.rb +++ b/lib/query/query_charges.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' module QueryCharges diff --git a/lib/query/query_custom_fields.rb b/lib/query/query_custom_fields.rb index 304c9a35..fec50508 100644 --- a/lib/query/query_custom_fields.rb +++ b/lib/query/query_custom_fields.rb @@ -1,20 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' module QueryCustomFields - - # Find all duplicate custom field joins on supporters # Returns an array of groups of duplicate custom_field_join_ids def self.find_dupes(np_id) - Qx.select("ARRAY_AGG(custom_field_joins.id)") + Qx.select('ARRAY_AGG(custom_field_joins.id)') .from(:custom_field_joins) - .join(:custom_field_masters, "custom_field_masters.id=custom_field_joins.custom_field_master_id") - .where("custom_field_masters.nonprofit_id=$id", id: np_id) - .group_by("custom_field_joins.custom_field_master_id", "custom_field_joins.value", "custom_field_joins.supporter_id") - .having("COUNT(custom_field_joins.id) > 1") + .join(:custom_field_masters, 'custom_field_masters.id=custom_field_joins.custom_field_master_id') + .where('custom_field_masters.nonprofit_id=$id', id: np_id) + .group_by('custom_field_joins.custom_field_master_id', 'custom_field_joins.value', 'custom_field_joins.supporter_id') + .having('COUNT(custom_field_joins.id) > 1') .execute(format: 'csv')[1..-1] end - end diff --git a/lib/query/query_donations.rb b/lib/query/query_donations.rb index 5c4a4317..9c7d106c 100644 --- a/lib/query/query_donations.rb +++ b/lib/query/query_donations.rb @@ -1,77 +1,77 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'query/query_supporters' module QueryDonations - # Export all donation data for a given campaign - def self.campaign_export(campaign_id) + def self.campaign_export(campaign_id) Psql.execute_vectors( Qexpr.new.select([ 'donations.created_at', - '(payments.gross_amount/100.00)::money::text AS amount', - "COUNT(recurring_donations.id) > 0 AS recurring", - "STRING_AGG(campaign_gift_options.name, ',') AS campaign_gift_names" + '(payments.gross_amount/100.00)::money::text AS amount', + 'COUNT(recurring_donations.id) > 0 AS recurring', + "STRING_AGG(campaign_gift_options.name, ',') AS campaign_gift_names" ].concat(QuerySupporters.supporter_export_selections) .concat([ - "supporters.id AS \"Supporter ID\"", - ]).concat([ - "coalesce(donations.designation, 'None') AS designation", - "#{QueryPayments.get_dedication_or_empty('type')}::text AS \"Dedication Type\"", - "#{QueryPayments.get_dedication_or_empty('name')}::text AS \"Dedicated To: Name\"", - "#{QueryPayments.get_dedication_or_empty('supporter_id')}::text AS \"Dedicated To: Supporter ID\"", - "#{QueryPayments.get_dedication_or_empty('contact', 'email')}::text AS \"Dedicated To: Email\"", - "#{QueryPayments.get_dedication_or_empty('contact', "phone")}::text AS \"Dedicated To: Phone\"", - "#{QueryPayments.get_dedication_or_empty( "contact", "address")}::text AS \"Dedicated To: Address\"", - "#{QueryPayments.get_dedication_or_empty( "note")}::text AS \"Dedicated To: Note\"", - "donations.campaign_id AS \"Campaign Id\"", - "users.email AS \"Campaign Creator Email\"" - ]) - ).from(:donations) - .join(:supporters, "supporters.id=donations.supporter_id") - .left_outer_join(:campaign_gifts, "campaign_gifts.donation_id=donations.id") - .left_outer_join(:campaign_gift_options, "campaign_gift_options.id=campaign_gifts.campaign_gift_option_id") - .left_outer_join(:recurring_donations, "recurring_donations.donation_id = donations.id") + 'supporters.id AS "Supporter ID"' + ]).concat([ + "coalesce(donations.designation, 'None') AS designation", + "#{QueryPayments.get_dedication_or_empty('type')}::text AS \"Dedication Type\"", + "#{QueryPayments.get_dedication_or_empty('name')}::text AS \"Dedicated To: Name\"", + "#{QueryPayments.get_dedication_or_empty('supporter_id')}::text AS \"Dedicated To: Supporter ID\"", + "#{QueryPayments.get_dedication_or_empty('contact', 'email')}::text AS \"Dedicated To: Email\"", + "#{QueryPayments.get_dedication_or_empty('contact', 'phone')}::text AS \"Dedicated To: Phone\"", + "#{QueryPayments.get_dedication_or_empty('contact', 'address')}::text AS \"Dedicated To: Address\"", + "#{QueryPayments.get_dedication_or_empty('note')}::text AS \"Dedicated To: Note\"", + 'donations.campaign_id AS "Campaign Id"', + 'users.email AS "Campaign Creator Email"' + ])).from(:donations) + .join(:supporters, 'supporters.id=donations.supporter_id') + .left_outer_join(:campaign_gifts, 'campaign_gifts.donation_id=donations.id') + .left_outer_join(:campaign_gift_options, 'campaign_gift_options.id=campaign_gifts.campaign_gift_option_id') + .left_outer_join(:recurring_donations, 'recurring_donations.donation_id = donations.id') .join_lateral(:payments, get_first_payment_for_donation.parse, true) .join(Qx.select('id, profile_id').from('campaigns') .where("id IN (#{QueryCampaigns .get_campaign_and_children(campaign_id) .parse})").as('campaigns').parse, - 'donations.campaign_id=campaigns.id') + 'donations.campaign_id=campaigns.id') .join(Qx.select('users.id, profiles.id AS profiles_id, users.email') .from('users') .add_join('profiles', 'profiles.user_id = users.id') - .as("users").parse, "users.profiles_id=campaigns.profile_id") - .group_by("donations.id", "supporters.id", "payments.id", "payments.gross_amount", "users.email") - .order_by("donations.date") + .as('users').parse, 'users.profiles_id=campaigns.profile_id') + .group_by('donations.id', 'supporters.id', 'payments.id', 'payments.gross_amount', 'users.email') + .order_by('donations.date') ) - end + end def self.for_campaign_activities(id) QueryDonations.activities_expression(['donations.recurring']) - .where("donations.campaign_id IN (#{QueryCampaigns + .where("donations.campaign_id IN (#{QueryCampaigns .get_campaign_and_children(id) .parse})") - .execute + .execute end def self.activities_expression(additional_selects) selects = [" - CASE + CASE WHEN donations.anonymous='t' - OR supporters.anonymous='t' - OR supporters.name='' - OR supporters.name IS NULL - THEN 'A supporter' - ELSE supporters.name - END AS supporter_name", - "(donations.amount / 100.0)::money::text as amount", - "donations.date"] + (additional_selects ? additional_selects : []) + OR supporters.anonymous='t' + OR supporters.name='' + OR supporters.name IS NULL + THEN 'A supporter' + ELSE supporters.name + END AS supporter_name", + '(donations.amount / 100.0)::money::text as amount', + 'donations.date'] + (additional_selects || []) - return Qx.select(selects.join(',')) + Qx.select(selects.join(',')) .from(:donations) .left_join(:supporters, 'donations.supporter_id=supporters.id') - .order_by("donations.date desc") + .order_by('donations.date desc') .limit(15) end @@ -79,21 +79,20 @@ module QueryDonations # !!! Note this returns the PAYMENT_IDS for each offsite donation def self.dupe_offsite_donations(np_id) payment_ids = Psql.execute_vectors( - Qexpr.new.select("ARRAY_AGG(payments.id) AS ids") - .from("donations") - .join(:offsite_payments, "offsite_payments.donation_id=donations.id") - .join(:payments, "payments.donation_id=donations.id") - .where("donations.nonprofit_id=$id", id: np_id) - .group_by("donations.supporter_id", "donations.amount", "donations.date") - .having("COUNT(donations.id) > 1") + Qexpr.new.select('ARRAY_AGG(payments.id) AS ids') + .from('donations') + .join(:offsite_payments, 'offsite_payments.donation_id=donations.id') + .join(:payments, 'payments.donation_id=donations.id') + .where('donations.nonprofit_id=$id', id: np_id) + .group_by('donations.supporter_id', 'donations.amount', 'donations.date') + .having('COUNT(donations.id) > 1') )[1..-1].map(&:flatten) end - def self.get_first_payment_for_donation(table_selector='donations') + def self.get_first_payment_for_donation(table_selector = 'donations') Qx.select('payments.id, payments.gross_amount').from(:payments) - .where("payments.donation_id = #{table_selector}.id") - .order_by('payments.created_at ASC') - .limit(1) + .where("payments.donation_id = #{table_selector}.id") + .order_by('payments.created_at ASC') + .limit(1) end - end diff --git a/lib/query/query_email_settings.rb b/lib/query/query_email_settings.rb index 2d1c4171..b2f6e639 100644 --- a/lib/query/query_email_settings.rb +++ b/lib/query/query_email_settings.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryEmailSettings - - Settings = ['notify_payments', 'notify_campaigns', 'notify_events', 'notify_payouts', 'notify_recurring_donations'] + Settings = %w[notify_payments notify_campaigns notify_events notify_payouts notify_recurring_donations].freeze def self.fetch(np_id, user_id) - es = Psql.execute(%Q( + es = Psql.execute(%( SELECT * FROM email_settings WHERE nonprofit_id=#{Qexpr.quote(np_id.to_i)} @@ -13,14 +14,14 @@ module QueryEmailSettings # If the user's event_settings table does not exist, return a hash with all settings true if es.nil? - es = Psql.execute(%Q( + es = Psql.execute(%( SELECT column_name FROM information_schema.columns WHERE table_name='email_settings' - )).map{|h| h['column_name']} - .reject{|name| ['id', 'nonprofit_id', 'user_id'].include?(name)} - .reduce({}){|h, name| h[name] = true; h} + )).map { |h| h['column_name'] } + .reject { |name| %w[id nonprofit_id user_id].include?(name) } + .each_with_object({}) { |name, h| h[name] = true; } end - return es + es end end diff --git a/lib/query/query_event_discounts.rb b/lib/query/query_event_discounts.rb index 1ab648a7..b0bb4d87 100644 --- a/lib/query/query_event_discounts.rb +++ b/lib/query/query_event_discounts.rb @@ -1,15 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryEventDiscounts - def self.with_event_ids(event_ids) return [] if event_ids.empty? - x = Psql.execute( - Qexpr.new.select("name", "id", "percent", "code", "created_at") - .from("event_discounts") - .where("event_discounts.event_id IN ($ids)", ids: event_ids) - .order_by("created_at DESC"), - ).map{|h| HashWithIndifferentAccess.new(h)} + x = Psql.execute( + Qexpr.new.select('name', 'id', 'percent', 'code', 'created_at') + .from('event_discounts') + .where('event_discounts.event_id IN ($ids)', ids: event_ids) + .order_by('created_at DESC') + ).map { |h| HashWithIndifferentAccess.new(h) } end - end diff --git a/lib/query/query_event_metrics.rb b/lib/query/query_event_metrics.rb index 5d555685..21fdb0be 100644 --- a/lib/query/query_event_metrics.rb +++ b/lib/query/query_event_metrics.rb @@ -1,47 +1,49 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryEventMetrics - - def self.expression(additional_selects=[]) + def self.expression(additional_selects = []) selects = [ - "coalesce(tickets.total, 0) AS total_attendees", - "coalesce(tickets.checked_in_count, 0) AS checked_in_count", - "coalesce(ticket_payments.total_paid, 0) AS tickets_total_paid", - "coalesce(donations.payment_total, 0) AS donations_total_paid", - "coalesce(ticket_payments.total_paid, 0) + coalesce(donations.payment_total, 0) AS total_paid" + 'coalesce(tickets.total, 0) AS total_attendees', + 'coalesce(tickets.checked_in_count, 0) AS checked_in_count', + 'coalesce(ticket_payments.total_paid, 0) AS tickets_total_paid', + 'coalesce(donations.payment_total, 0) AS donations_total_paid', + 'coalesce(ticket_payments.total_paid, 0) + coalesce(donations.payment_total, 0) AS total_paid' ] - tickets_sub = Qx.select("event_id", "SUM(quantity) AS total", "SUM(tickets.checked_in::int) AS checked_in_count") - .from("tickets") - .group_by("event_id") - .as("tickets") - - ticket_payments_subquery = Qx.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tickets") + tickets_sub = Qx.select('event_id', 'SUM(quantity) AS total', 'SUM(tickets.checked_in::int) AS checked_in_count') + .from('tickets') + .group_by('event_id') + .as('tickets') - ticket_payments_sub = Qx.select("SUM(payments.gross_amount) AS total_paid", "tickets.event_id") - .from(:payments) - .join(ticket_payments_subquery, "payments.id=tickets.payment_id") - .group_by("tickets.event_id") - .as("ticket_payments") + ticket_payments_subquery = Qx.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').group_by('payment_id').as('tickets') - donations_sub = Qx.select("event_id", "SUM(payments.gross_amount) as payment_total") - .from("donations") - .group_by("event_id") - .left_join("payments", "donations.id=payments.donation_id") - .as("donations") + ticket_payments_sub = Qx.select('SUM(payments.gross_amount) AS total_paid', 'tickets.event_id') + .from(:payments) + .join(ticket_payments_subquery, 'payments.id=tickets.payment_id') + .group_by('tickets.event_id') + .as('ticket_payments') + + donations_sub = Qx.select('event_id', 'SUM(payments.gross_amount) as payment_total') + .from('donations') + .group_by('event_id') + .left_join('payments', 'donations.id=payments.donation_id') + .as('donations') selects = selects.concat(additional_selects) - return Qx.select(*selects) - .from("events") - .left_join( - [tickets_sub, "tickets.event_id = events.id"], - [donations_sub, "donations.event_id = events.id"], - [ticket_payments_sub, "ticket_payments.event_id=events.id"] + Qx.select(*selects) + .from('events') + .left_join( + [tickets_sub, 'tickets.event_id = events.id'], + [donations_sub, 'donations.event_id = events.id'], + [ticket_payments_sub, 'ticket_payments.event_id=events.id'] ) end def self.with_event_ids(event_ids) return [] if event_ids.empty? - QueryEventMetrics.expression().where("events.id in ($ids)", ids: event_ids).execute + + QueryEventMetrics.expression.where('events.id in ($ids)', ids: event_ids).execute end def self.for_listings(id_type, id, params) @@ -61,29 +63,25 @@ module QueryEventMetrics exp = QueryEventMetrics.expression(selects) if id_type == 'profile' - exp = exp.and_where(["events.profile_id = $id", id: id]) + exp = exp.and_where(['events.profile_id = $id', id: id]) end if id_type == 'nonprofit' - exp = exp.and_where(["events.nonprofit_id = $id", id: id]) + exp = exp.and_where(['events.nonprofit_id = $id', id: id]) end if params['active'].present? exp = exp - .and_where(['events.end_datetime >= $date', date: Time.now]) - .and_where(["events.published = TRUE AND coalesce(events.deleted, FALSE) = FALSE"]) + .and_where(['events.end_datetime >= $date', date: Time.now]) + .and_where(['events.published = TRUE AND coalesce(events.deleted, FALSE) = FALSE']) end if params['past'].present? exp = exp - .and_where(['events.end_datetime < $date', date: Time.now]) - .and_where(["events.published = TRUE AND coalesce(events.deleted, FALSE) = FALSE"]) + .and_where(['events.end_datetime < $date', date: Time.now]) + .and_where(['events.published = TRUE AND coalesce(events.deleted, FALSE) = FALSE']) end if params['unpublished'].present? - exp = exp.and_where(["coalesce(events.published, FALSE) = FALSE AND coalesce(events.deleted, FALSE) = FALSE"]) + exp = exp.and_where(['coalesce(events.published, FALSE) = FALSE AND coalesce(events.deleted, FALSE) = FALSE']) end - if params['deleted'].present? - exp = exp.and_where(["events.deleted = TRUE"]) - end - return exp.execute + exp = exp.and_where(['events.deleted = TRUE']) if params['deleted'].present? + exp.execute end - end - diff --git a/lib/query/query_event_organizer.rb b/lib/query/query_event_organizer.rb index c70507b1..2bac4d1b 100644 --- a/lib/query/query_event_organizer.rb +++ b/lib/query/query_event_organizer.rb @@ -1,15 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryEventOrganizer - def self.with_event(event_id) + def self.with_event(event_id) Qx.select( - "coalesce(profiles.name, nonprofits.name) AS name", - "coalesce(users.email, nonprofits.email) AS email" - ) + 'coalesce(profiles.name, nonprofits.name) AS name', + 'coalesce(users.email, nonprofits.email) AS email' + ) .from(:events) - .left_join(:profiles, "profiles.id=events.profile_id") - .add_left_join(:users, "profiles.user_id=users.id") - .add_join(:nonprofits, "events.nonprofit_id=nonprofits.id") - .where("events.id=$id", id: event_id) + .left_join(:profiles, 'profiles.id=events.profile_id') + .add_left_join(:users, 'profiles.user_id=users.id') + .add_join(:nonprofits, 'events.nonprofit_id=nonprofits.id') + .where('events.id=$id', id: event_id) .execute.first - end + end end diff --git a/lib/query/query_events.rb b/lib/query/query_events.rb index 252236c7..37a3af5a 100644 --- a/lib/query/query_events.rb +++ b/lib/query/query_events.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' @@ -5,13 +7,13 @@ module QueryEvents def self.name_and_id(npo_id) Psql.execute( Qexpr.new.select( - "events.name", - "events.id") - .from("events") - .where("events.nonprofit_id=$id", id: npo_id) + 'events.name', + 'events.id' + ) + .from('events') + .where('events.nonprofit_id=$id', id: npo_id) .where("events.deleted='f' OR events.deleted IS NULL") - .order_by("events.name ASC") + .order_by('events.name ASC') ) end - end diff --git a/lib/query/query_full_contact_infos.rb b/lib/query/query_full_contact_infos.rb index dc288246..e8c3f134 100644 --- a/lib/query/query_full_contact_infos.rb +++ b/lib/query/query_full_contact_infos.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' module QueryFullContactInfos - def self.fetch_associated_tables(fc_info_id) - photo = Psql.execute( Qexpr.new.from(:full_contact_photos).select('url').where("full_contact_info_id = $id", id: fc_info_id).where("is_primary")) - orgs = Psql.execute( Qexpr.new.from(:full_contact_orgs).select('current', 'name', 'title', 'start_date', 'end_date').where("full_contact_info_id = $id", id: fc_info_id).order_by('start_date DESC NULLS LAST')) - topics = Psql.execute( Qexpr.new.from(:full_contact_topics).select('value').where("full_contact_info_id = $id", id: fc_info_id).order_by('value ASC')) - profiles = Psql.execute( Qexpr.new.from(:full_contact_social_profiles).select('type_id', 'followers', 'url').where("full_contact_info_id = $id", id: fc_info_id).order_by('type_id ASC')) - return { + photo = Psql.execute(Qexpr.new.from(:full_contact_photos).select('url').where('full_contact_info_id = $id', id: fc_info_id).where('is_primary')) + orgs = Psql.execute(Qexpr.new.from(:full_contact_orgs).select('current', 'name', 'title', 'start_date', 'end_date').where('full_contact_info_id = $id', id: fc_info_id).order_by('start_date DESC NULLS LAST')) + topics = Psql.execute(Qexpr.new.from(:full_contact_topics).select('value').where('full_contact_info_id = $id', id: fc_info_id).order_by('value ASC')) + profiles = Psql.execute(Qexpr.new.from(:full_contact_social_profiles).select('type_id', 'followers', 'url').where('full_contact_info_id = $id', id: fc_info_id).order_by('type_id ASC')) + { photo: photo, topics: topics, orgs: orgs, diff --git a/lib/query/query_nonprofit_keys.rb b/lib/query/query_nonprofit_keys.rb index a3abd041..3e0c9bef 100644 --- a/lib/query/query_nonprofit_keys.rb +++ b/lib/query/query_nonprofit_keys.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module QueryNonprofitKeys - def self.get_key(npo_id, key_name) query = Qx.select(key_name) - .from(:nonprofit_keys) - .where({'nonprofit_id' => npo_id}) - .execute - raise ActiveRecord::RecordNotFound.new("Nonprofit key does not exist: #{key_name}") if query.empty? - Cypher.decrypt(JSON.parse query.first[key_name]) - end + .from(:nonprofit_keys) + .where('nonprofit_id' => npo_id) + .execute + raise ActiveRecord::RecordNotFound, "Nonprofit key does not exist: #{key_name}" if query.empty? + Cypher.decrypt(JSON.parse(query.first[key_name])) + end end diff --git a/lib/query/query_nonprofits.rb b/lib/query/query_nonprofits.rb index bd8b069c..7e1e4a39 100644 --- a/lib/query/query_nonprofits.rb +++ b/lib/query/query_nonprofits.rb @@ -1,26 +1,27 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' require 'psql' module QueryNonprofits - def self.all_that_need_payouts Psql.execute_vectors( Qexpr.new.select( - "nonprofits.id", - "nonprofits.stripe_account_id", + 'nonprofits.id', + 'nonprofits.stripe_account_id', "'support@commitchange.com' AS email", "'192.168.0.1' AS user_ip", - "bank_accounts.name" + 'bank_accounts.name' ).from(:nonprofits) .where("nonprofits.verification_status='verified'") - .join(:bank_accounts, "bank_accounts.nonprofit_id=nonprofits.id") + .join(:bank_accounts, 'bank_accounts.nonprofit_id=nonprofits.id') .where("bank_accounts.pending_verification='f'") .join( - Qexpr.new.select("nonprofit_id") - .from(:charges).group_by("nonprofit_id") - .where("status='available'").as("charges"), - "charges.nonprofit_id=nonprofits.id" + Qexpr.new.select('nonprofit_id') + .from(:charges).group_by('nonprofit_id') + .where("status='available'").as('charges'), + 'charges.nonprofit_id=nonprofits.id' ) )[1..-1] end @@ -28,18 +29,16 @@ module QueryNonprofits def self.by_search_string(string) results = Psql.execute_vectors( Qexpr.new.select( - "nonprofits.id", - "nonprofits.name" + 'nonprofits.id', + 'nonprofits.name' ).from(:nonprofits) - .where("lower(nonprofits.name) LIKE lower($search)", search: "%#{string}%") + .where('lower(nonprofits.name) LIKE lower($search)', search: "%#{string}%") .where("nonprofits.published='t'") - .order_by("nonprofits.name ASC") + .order_by('nonprofits.name ASC') .limit(10) )[1..-1] - if results - results = results.map {|id, name| {id: id, name: name}} - end - return results + results = results.map { |id, name| { id: id, name: name } } if results + results end def self.for_admin(params) @@ -52,66 +51,67 @@ module QueryNonprofits 'nonprofits.verification_status', 'nonprofits.vetted', 'nonprofits.stripe_account_id', - 'coalesce(events.count, 0) AS events_count', - 'coalesce(campaigns.count, 0) AS campaigns_count', + 'coalesce(events.count, 0) AS events_count', + 'coalesce(campaigns.count, 0) AS campaigns_count', 'billing_plans.percentage_fee', 'cards.stripe_customer_id', 'charges.total_processed', 'charges.total_fees' ).from(:nonprofits) - .add_left_join(:cards, "cards.holder_id=nonprofits.id AND cards.holder_type='Nonprofit'") - .add_left_join(:billing_subscriptions, "billing_subscriptions.nonprofit_id=nonprofits.id") - .add_left_join(:billing_plans, "billing_subscriptions.billing_plan_id=billing_plans.id") - .add_left_join( - Qx.select( - "((SUM(coalesce(fee, 0)) * .978) / 100)::money::text AS total_fees", - "(SUM(coalesce(amount, 0)) / 100)::money::text AS total_processed", - "nonprofit_id") - .from(:charges) - .where("status != 'failed'") - .and_where("created_at::date >= '2017-03-15'") - .group_by("nonprofit_id") - .as("charges"), - "charges.nonprofit_id=nonprofits.id" - ) - .add_left_join( - Qx.select("COUNT(id)", "nonprofit_id") - .from(:events) - .group_by("nonprofit_id") - .as("events"), - "events.nonprofit_id=nonprofits.id" - ) - .add_left_join( - Qx.select("COUNT(id)", "nonprofit_id") - .from(:campaigns) - .group_by("nonprofit_id") - .as("campaigns"), - "campaigns.nonprofit_id=nonprofits.id" - ) - .paginate(params[:page].to_i, params[:page_length].to_i) - .order_by('nonprofits.created_at DESC') + .add_left_join(:cards, "cards.holder_id=nonprofits.id AND cards.holder_type='Nonprofit'") + .add_left_join(:billing_subscriptions, 'billing_subscriptions.nonprofit_id=nonprofits.id') + .add_left_join(:billing_plans, 'billing_subscriptions.billing_plan_id=billing_plans.id') + .add_left_join( + Qx.select( + '((SUM(coalesce(fee, 0)) * .978) / 100)::money::text AS total_fees', + '(SUM(coalesce(amount, 0)) / 100)::money::text AS total_processed', + 'nonprofit_id' + ) + .from(:charges) + .where("status != 'failed'") + .and_where("created_at::date >= '2017-03-15'") + .group_by('nonprofit_id') + .as('charges'), + 'charges.nonprofit_id=nonprofits.id' + ) + .add_left_join( + Qx.select('COUNT(id)', 'nonprofit_id') + .from(:events) + .group_by('nonprofit_id') + .as('events'), + 'events.nonprofit_id=nonprofits.id' + ) + .add_left_join( + Qx.select('COUNT(id)', 'nonprofit_id') + .from(:campaigns) + .group_by('nonprofit_id') + .as('campaigns'), + 'campaigns.nonprofit_id=nonprofits.id' + ) + .paginate(params[:page].to_i, params[:page_length].to_i) + .order_by('nonprofits.created_at DESC') - if params[:search].present? - expr = expr.where(%Q( + if params[:search].present? + expr = expr.where(%( nonprofits.name ILIKE $search OR nonprofits.email ILIKE $search OR nonprofits.city ILIKE $search ), search: '%' + params[:search] + '%') - end + end - return expr.execute + expr.execute end - def self.find_nonprofits_with_no_payments() + def self.find_nonprofits_with_no_payments Nonprofit.includes(:payments).where('payments.nonprofit_id IS NULL') end def self.find_nonprofits_with_payments_in_last_n_days(days) - Payment.where("date >= ?", Time.now - days.days).pluck('nonprofit_id').to_a.uniq + Payment.where('date >= ?', Time.now - days.days).pluck('nonprofit_id').to_a.uniq end def self.find_nonprofits_with_payments_but_not_in_last_n_days(days) recent_nonprofits = find_nonprofits_with_payments_in_last_n_days(days) - Payment.where("date < ?", Time.now - days.days).pluck('nonprofit_id').to_a.uniq.select{|i| !recent_nonprofits.include?(i)} + Payment.where('date < ?', Time.now - days.days).pluck('nonprofit_id').to_a.uniq.reject { |i| recent_nonprofits.include?(i) } end end diff --git a/lib/query/query_payments.rb b/lib/query/query_payments.rb index f535e791..203251d3 100644 --- a/lib/query/query_payments.rb +++ b/lib/query/query_payments.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' require 'psql' @@ -6,8 +8,6 @@ require 'query/query_supporters' require 'active_support/core_ext' module QueryPayments - - # Fetch all payments connected to available charges, undisbursed refunds or lost disputes # Ids For Payouts collects all payments where: # *they have a connected charge, refund or dispute (CRD), i.e. the CRD's payment_id is not NULL and represents a record in payments @@ -20,38 +20,39 @@ module QueryPayments # * For refunds and disputes, the gross_amount should be negative since we're decreasing the nonprofit's balance # # In effect, we're getting the list of payments which haven't been paid out in a some fashion. This is not a great design but it works mostly. - def self.ids_for_payout(npo_id, options={}) + def self.ids_for_payout(npo_id, options = {}) end_of_day = (Time.current + 1.day).beginning_of_day Qx.select('DISTINCT payments.id') - .from(:payments) - .left_join(:charges, 'charges.payment_id=payments.id') - .add_left_join(:refunds, 'refunds.payment_id=payments.id') - .add_left_join(:disputes, 'disputes.payment_id=payments.id') - .where('payments.nonprofit_id=$id', id: npo_id) - .and_where("refunds.payment_id IS NOT NULL OR charges.payment_id IS NOT NULL OR disputes.payment_id IS NOT NULL") - .and_where(%Q( + .from(:payments) + .left_join(:charges, 'charges.payment_id=payments.id') + .add_left_join(:refunds, 'refunds.payment_id=payments.id') + .add_left_join(:disputes, 'disputes.payment_id=payments.id') + .where('payments.nonprofit_id=$id', id: npo_id) + .and_where('refunds.payment_id IS NOT NULL OR charges.payment_id IS NOT NULL OR disputes.payment_id IS NOT NULL') + .and_where(%( ((refunds.payment_id IS NOT NULL AND refunds.disbursed IS NULL) OR refunds.disbursed='f') OR (charges.status='available') OR (disputes.status='lost') )) - .and_where("payments.date <= $date", date: options[:date] || end_of_day) - .execute.map{|h| h['id']} + .and_where('payments.date <= $date', date: options[:date] || end_of_day) + .execute.map { |h| h['id'] } end # the amount to payout calculates the total payout based upon the payments it's provided, likely provided from ids_to_payout def self.get_payout_totals(payment_ids) - return {'gross_amount' => 0, 'fee_total' => 0, 'net_amount' => 0} if payment_ids.empty? + return { 'gross_amount' => 0, 'fee_total' => 0, 'net_amount' => 0 } if payment_ids.empty? + Qx.select( 'SUM(payments.gross_amount) AS gross_amount', 'SUM(payments.fee_total) AS fee_total', 'SUM(payments.net_amount) AS net_amount', - 'COUNT(payments.*) AS count') + 'COUNT(payments.*) AS count' + ) .from(:payments) - .where("payments.id IN ($ids)", ids: payment_ids) + .where('payments.id IN ($ids)', ids: payment_ids) .execute.first end - def self.nonprofit_balances(npo_id) Psql.execute( Qexpr.new.select( @@ -60,17 +61,17 @@ module QueryPayments 'COUNT(available) AS count_available', 'COUNT(pending) AS count_pending', 'COUNT(refunds) AS count_refunds', - 'COUNT(disputes) AS count_disputes') + 'COUNT(disputes) AS count_disputes' + ) .from(:payments) .left_outer_join('refunds', "refunds.payment_id=payments.id AND (refunds.disbursed='f' OR refunds.disbursed IS NULL)") - .left_outer_join("charges available", "available.status='available' AND available.payment_id=payments.id") - .left_outer_join("charges pending", "pending.status='pending' AND pending.payment_id=payments.id") - .left_outer_join("disputes", "disputes.status='lost' AND disputes.payment_id=payments.id") - .where("payments.nonprofit_id=$id", id: npo_id) + .left_outer_join('charges available', "available.status='available' AND available.payment_id=payments.id") + .left_outer_join('charges pending', "pending.status='pending' AND pending.payment_id=payments.id") + .left_outer_join('disputes', "disputes.status='lost' AND disputes.payment_id=payments.id") + .where('payments.nonprofit_id=$id', id: npo_id) ).first end - def self.full_search(npo_id, query) limit = 30 offset = Qexpr.page_offset(limit, query[:page]) @@ -87,84 +88,79 @@ module QueryPayments payments = Psql.execute(expr.limit(limit).offset(offset).parse) totals_query = expr - .remove(:select) - .remove(:order_by) - .select( - 'COALESCE(COUNT(payments.id), 0) AS count', - 'COALESCE((SUM(payments.gross_amount) / 100.0), 0)::money::text AS amount') + .remove(:select) + .remove(:order_by) + .select( + 'COALESCE(COUNT(payments.id), 0) AS count', + 'COALESCE((SUM(payments.gross_amount) / 100.0), 0)::money::text AS amount' + ) totals = Psql.execute(totals_query).first - return { + { data: payments, total_count: totals['count'], total_amount: totals['amount'], remaining: Qexpr.remaining_count(totals['count'], limit, query[:page]) } - end - # we must provide payments.*, supporters.*, donations.*, associated event_id, associated campaign_id def self.full_search_expr(npo_id, query) expr = Qexpr.new.from('payments') - .left_outer_join('supporters', "supporters.id=payments.supporter_id") - .left_outer_join('donations', 'donations.id=payments.donation_id' ) - .join("(#{select_to_filter_search(npo_id, query)}) AS \"filtered_payments\"", 'payments.id = filtered_payments.id') - .order_by('payments.date DESC') + .left_outer_join('supporters', 'supporters.id=payments.supporter_id') + .left_outer_join('donations', 'donations.id=payments.donation_id') + .join("(#{select_to_filter_search(npo_id, query)}) AS \"filtered_payments\"", 'payments.id = filtered_payments.id') + .order_by('payments.date DESC') - if ['asc', 'desc'].include? query[:sort_amount] + if %w[asc desc].include? query[:sort_amount] expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") end - if ['asc', 'desc'].include? query[:sort_date] + if %w[asc desc].include? query[:sort_date] expr = expr.order_by("payments.date #{query[:sort_date]}") end - if ['asc', 'desc'].include? query[:sort_name] + if %w[asc desc].include? query[:sort_name] expr = expr.order_by("coalesce(NULLIF(supporters.name, ''), NULLIF(supporters.email, '')) #{query[:sort_name]}") end - if ['asc', 'desc'].include? query[:sort_type] + if %w[asc desc].include? query[:sort_type] expr = expr.order_by("payments.kind #{query[:sort_type]}") end - if ['asc', 'desc'].include? query[:sort_towards] + if %w[asc desc].include? query[:sort_towards] expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") end - return expr + expr end # perform the search but only get the relevant payment_ids def self.select_to_filter_search(npo_id, query) - inner_donation_search = Qexpr.new.select('donations.*').from('donations') - if (query[:event_id].present?) + if query[:event_id].present? inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id]) end - if (query[:campaign_id].present?) + if query[:campaign_id].present? campaign_search = campaign_and_child_query_as_raw_string inner_donation_search = inner_donation_search.where("donations.campaign_id IN (#{campaign_search})", id: query[:campaign_id]) end expr = Qexpr.new.select('payments.id').from('payments') - .left_outer_join('supporters', "supporters.id=payments.supporter_id") - .left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments.donation_id' ) - .where('payments.nonprofit_id=$id', id: npo_id.to_i) + .left_outer_join('supporters', 'supporters.id=payments.supporter_id') + .left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments.donation_id') + .where('payments.nonprofit_id=$id', id: npo_id.to_i) - - if query[:search].present? - expr = SearchVector.query(query[:search], expr) - end - if ['asc', 'desc'].include? query[:sort_amount] + expr = SearchVector.query(query[:search], expr) if query[:search].present? + if %w[asc desc].include? query[:sort_amount] expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") end - if ['asc', 'desc'].include? query[:sort_date] + if %w[asc desc].include? query[:sort_date] expr = expr.order_by("payments.date #{query[:sort_date]}") end - if ['asc', 'desc'].include? query[:sort_name] + if %w[asc desc].include? query[:sort_name] expr = expr.order_by("coalesce(NULLIF(supporters.name, ''), NULLIF(supporters.email, '')) #{query[:sort_name]}") end - if ['asc', 'desc'].include? query[:sort_type] + if %w[asc desc].include? query[:sort_type] expr = expr.order_by("payments.kind #{query[:sort_type]}") end - if ['asc', 'desc'].include? query[:sort_towards] + if %w[asc desc].include? query[:sort_towards] expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") end if query[:after_date].present? @@ -183,10 +179,10 @@ module QueryPayments expr = expr.where("to_char(payments.date, 'YYYY')=$year", year: query[:year]) end if query[:designation].present? - expr = expr.where("donations.designation @@ $s", s: "#{query[:designation]}") + expr = expr.where('donations.designation @@ $s', s: (query[:designation]).to_s) end if query[:dedication].present? - expr = expr.where("donations.dedication @@ $s", s: "#{query[:dedication]}") + expr = expr.where('donations.dedication @@ $s', s: (query[:dedication]).to_s) end if query[:donation_type].present? expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(',')) @@ -194,66 +190,61 @@ module QueryPayments if query[:campaign_id].present? campaign_search = campaign_and_child_query_as_raw_string expr = expr - .left_outer_join("campaigns", "campaigns.id=donations.campaign_id" ) - .where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id]) + .left_outer_join('campaigns', 'campaigns.id=donations.campaign_id') + .where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id]) end if query[:event_id].present? - tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix") + tickets_subquery = Qexpr.new.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by('payment_id').as('tix') expr = expr - .left_outer_join(tickets_subquery, "tix.payment_id=payments.id") - .where("tix.event_id=$id OR donations.event_id=$id", id: query[:event_id]) + .left_outer_join(tickets_subquery, 'tix.payment_id=payments.id') + .where('tix.event_id=$id OR donations.event_id=$id', id: query[:event_id]) end expr = expr - #we have the first part of the search. We need to create the second in certain situations + # we have the first part of the search. We need to create the second in certain situations filtered_payment_id_search = expr.parse if query[:event_id].present? || query[:campaign_id].present? - filtered_payment_id_search = filtered_payment_id_search + " UNION DISTINCT " + create_reverse_select(npo_id, query).parse + filtered_payment_id_search = filtered_payment_id_search + ' UNION DISTINCT ' + create_reverse_select(npo_id, query).parse end - - filtered_payment_id_search end # we use this when we need to get the refund info - def self.create_reverse_select(npo_id, query) + def self.create_reverse_select(npo_id, query) inner_donation_search = Qexpr.new.select('donations.*').from('donations') - if (query[:event_id].present?) + if query[:event_id].present? inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id]) end - if (query[:campaign_id].present?) + if query[:campaign_id].present? campaign_search = campaign_and_child_query_as_raw_string inner_donation_search = inner_donation_search.where("donations.campaign_id IN (#{campaign_search})", id: query[:campaign_id]) end expr = Qexpr.new.select('payments.id').from('payments') - .left_outer_join('supporters', "supporters.id=payments.supporter_id") + .left_outer_join('supporters', 'supporters.id=payments.supporter_id') .left_outer_join('refunds', 'payments.id=refunds.payment_id') - .left_outer_join('charges', 'refunds.charge_id=charges.id') - .left_outer_join('payments AS payments_orig', 'payments_orig.id=charges.payment_id') - .left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments_orig.donation_id' ) - .where('payments.nonprofit_id=$id', id: npo_id.to_i) + .left_outer_join('charges', 'refunds.charge_id=charges.id') + .left_outer_join('payments AS payments_orig', 'payments_orig.id=charges.payment_id') + .left_outer_join(inner_donation_search.as('donations'), 'donations.id=payments_orig.donation_id') + .where('payments.nonprofit_id=$id', id: npo_id.to_i) - - if query[:search].present? - expr = SearchVector.query(query[:search], expr) - end - if ['asc', 'desc'].include? query[:sort_amount] + expr = SearchVector.query(query[:search], expr) if query[:search].present? + if %w[asc desc].include? query[:sort_amount] expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") end - if ['asc', 'desc'].include? query[:sort_date] + if %w[asc desc].include? query[:sort_date] expr = expr.order_by("payments.date #{query[:sort_date]}") end - if ['asc', 'desc'].include? query[:sort_name] + if %w[asc desc].include? query[:sort_name] expr = expr.order_by("coalesce(NULLIF(supporters.name, ''), NULLIF(supporters.email, '')) #{query[:sort_name]}") end - if ['asc', 'desc'].include? query[:sort_type] + if %w[asc desc].include? query[:sort_type] expr = expr.order_by("payments.kind #{query[:sort_type]}") end - if ['asc', 'desc'].include? query[:sort_towards] + if %w[asc desc].include? query[:sort_towards] expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") end if query[:after_date].present? @@ -272,10 +263,10 @@ module QueryPayments expr = expr.where("to_char(payments.date, 'YYYY')=$year", year: query[:year]) end if query[:designation].present? - expr = expr.where("donations.designation @@ $s", s: "#{query[:designation]}") + expr = expr.where('donations.designation @@ $s', s: (query[:designation]).to_s) end if query[:dedication].present? - expr = expr.where("donations.dedication @@ $s", s: "#{query[:dedication]}") + expr = expr.where('donations.dedication @@ $s', s: (query[:dedication]).to_s) end if query[:donation_type].present? expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(',')) @@ -283,65 +274,60 @@ module QueryPayments if query[:campaign_id].present? campaign_search = campaign_and_child_query_as_raw_string expr = expr - .left_outer_join("campaigns", "campaigns.id=donations.campaign_id" ) - .where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id]) + .left_outer_join('campaigns', 'campaigns.id=donations.campaign_id') + .where("campaigns.id IN (#{campaign_search})", id: query[:campaign_id]) end if query[:event_id].present? - tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by("payment_id").as("tix") + tickets_subquery = Qexpr.new.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').where('tickets.event_id=$event_id', event_id: query[:event_id]).group_by('payment_id').as('tix') expr = expr - .left_outer_join(tickets_subquery, "tix.payment_id=payments_orig.id") - .where("tix.event_id=$id OR donations.event_id=$id", id: query[:event_id]) + .left_outer_join(tickets_subquery, 'tix.payment_id=payments_orig.id') + .where('tix.event_id=$id OR donations.event_id=$id', id: query[:event_id]) end expr end - def self.for_export_enumerable(npo_id, query, chunk_limit=35000) - ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, - query: {required:true, is_hash: true}}) + def self.for_export_enumerable(npo_id, query, chunk_limit = 35_000) + ParamValidation.new({ npo_id: npo_id, query: query }, npo_id: { required: true, is_int: true }, + query: { required: true, is_hash: true }) - return QexprQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| + QexprQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| get_chunk_of_export(npo_id, query, offset, limit, skip_header) end - end def self.for_export(npo_id, query) - tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tickets") + tickets_subquery = Qexpr.new.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').group_by('payment_id').as('tickets') expr = full_search_expr(npo_id, query) - .select(*export_selects) - .left_outer_join('campaign_gifts', 'campaign_gifts.donation_id=donations.id') - .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') - .left_outer_join("(#{campaigns_with_creator_email}) AS campaigns_for_export", 'donations.campaign_id=campaigns_for_export.id') - .left_outer_join(tickets_subquery, 'tickets.payment_id=payments.id') - .left_outer_join('events events_for_export', 'events_for_export.id=tickets.event_id OR donations.event_id=events_for_export.id') - .left_outer_join('offsite_payments', 'offsite_payments.payment_id=payments.id') - .parse + .select(*export_selects) + .left_outer_join('campaign_gifts', 'campaign_gifts.donation_id=donations.id') + .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') + .left_outer_join("(#{campaigns_with_creator_email}) AS campaigns_for_export", 'donations.campaign_id=campaigns_for_export.id') + .left_outer_join(tickets_subquery, 'tickets.payment_id=payments.id') + .left_outer_join('events events_for_export', 'events_for_export.id=tickets.event_id OR donations.event_id=events_for_export.id') + .left_outer_join('offsite_payments', 'offsite_payments.payment_id=payments.id') + .parse Psql.execute_vectors(expr) end - def self.get_chunk_of_export(npo_id, query, offset=nil, limit=nil, skip_header=false ) - - return QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) { - - - tickets_subquery = Qexpr.new.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tickets") + def self.get_chunk_of_export(npo_id, query, offset = nil, limit = nil, skip_header = false) + QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) do + tickets_subquery = Qexpr.new.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').group_by('payment_id').as('tickets') expr = full_search_expr(npo_id, query) - .select(*export_selects) - .left_outer_join('campaign_gifts', 'campaign_gifts.donation_id=donations.id') - .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') - .left_outer_join("(#{campaigns_with_creator_email}) AS campaigns_for_export", 'donations.campaign_id=campaigns_for_export.id') - .left_outer_join(tickets_subquery, 'tickets.payment_id=payments.id') - .left_outer_join('events events_for_export', 'events_for_export.id=tickets.event_id OR donations.event_id=events_for_export.id') - .left_outer_join('offsite_payments', 'offsite_payments.payment_id=payments.id') - } + .select(*export_selects) + .left_outer_join('campaign_gifts', 'campaign_gifts.donation_id=donations.id') + .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') + .left_outer_join("(#{campaigns_with_creator_email}) AS campaigns_for_export", 'donations.campaign_id=campaigns_for_export.id') + .left_outer_join(tickets_subquery, 'tickets.payment_id=payments.id') + .left_outer_join('events events_for_export', 'events_for_export.id=tickets.event_id OR donations.event_id=events_for_export.id') + .left_outer_join('offsite_payments', 'offsite_payments.payment_id=payments.id') + end end - def self.get_dedication_or_empty(*path) - "json_extract_path_text(coalesce(nullif(trim(both from donations.dedication), ''), '{}')::json, #{path.map{|i| "'#{i}'"}.join(',')})" + "json_extract_path_text(coalesce(nullif(trim(both from donations.dedication), ''), '{}')::json, #{path.map { |i| "'#{i}'" }.join(',')})" end def self.export_selects @@ -350,114 +336,109 @@ module QueryPayments '(payments.fee_total / 100.0)::money::text AS fee_total', '(payments.net_amount / 100.0)::money::text AS net_amount', 'payments.kind AS type'] - .concat(QuerySupporters.supporter_export_selections) - .concat([ - "coalesce(donations.designation, 'None') AS designation", - "#{get_dedication_or_empty('type')}::text AS \"Dedication Type\"", - "#{get_dedication_or_empty('name')}::text AS \"Dedicated To: Name\"", - "#{get_dedication_or_empty('supporter_id')}::text AS \"Dedicated To: Supporter ID\"", - "#{get_dedication_or_empty('contact', 'email')}::text AS \"Dedicated To: Email\"", - "#{get_dedication_or_empty('contact', "phone")}::text AS \"Dedicated To: Phone\"", - "#{get_dedication_or_empty( "contact", "address")}::text AS \"Dedicated To: Address\"", - "#{get_dedication_or_empty( "note")}::text AS \"Dedicated To: Note\"", - 'donations.anonymous', - 'donations.comment', - "coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign", - "campaigns_for_export.id AS \"Campaign Id\"", - "coalesce(nullif(campaigns_for_export.creator_email, ''), '') AS campaign_creator_email", - "coalesce(nullif(campaign_gift_options.name, ''), 'None') AS campaign_gift_level", - 'events_for_export.name AS event_name', - 'payments.id AS payment_id', - 'offsite_payments.check_number AS check_number', - 'donations.comment AS donation_note' - ]) + .concat(QuerySupporters.supporter_export_selections) + .concat([ + "coalesce(donations.designation, 'None') AS designation", + "#{get_dedication_or_empty('type')}::text AS \"Dedication Type\"", + "#{get_dedication_or_empty('name')}::text AS \"Dedicated To: Name\"", + "#{get_dedication_or_empty('supporter_id')}::text AS \"Dedicated To: Supporter ID\"", + "#{get_dedication_or_empty('contact', 'email')}::text AS \"Dedicated To: Email\"", + "#{get_dedication_or_empty('contact', 'phone')}::text AS \"Dedicated To: Phone\"", + "#{get_dedication_or_empty('contact', 'address')}::text AS \"Dedicated To: Address\"", + "#{get_dedication_or_empty('note')}::text AS \"Dedicated To: Note\"", + 'donations.anonymous', + 'donations.comment', + "coalesce(nullif(campaigns_for_export.name, ''), 'None') AS campaign", + 'campaigns_for_export.id AS "Campaign Id"', + "coalesce(nullif(campaigns_for_export.creator_email, ''), '') AS campaign_creator_email", + "coalesce(nullif(campaign_gift_options.name, ''), 'None') AS campaign_gift_level", + 'events_for_export.name AS event_name', + 'payments.id AS payment_id', + 'offsite_payments.check_number AS check_number', + 'donations.comment AS donation_note' + ]) end - # Create the data structure for the payout export CSVs # Has two sections: two rows for info about the payout, then all the rows after that are for the payments # TODO reuse the standard payment export query for the payment rows for this query def self.for_payout(npo_id, payout_id) - tickets_subquery = Qx.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tickets") + tickets_subquery = Qx.select('payment_id', 'MAX(event_id) AS event_id').from('tickets').group_by('payment_id').as('tickets') supporters_subq = Qx.select(QuerySupporters.supporter_export_selections) Qx.select( - "to_char(payouts.created_at, 'MM/DD/YYYY HH24:MIam') AS date", - "(payouts.gross_amount / 100.0)::money::text AS gross_total", - "(payouts.fee_total / 100.0)::money::text AS fee_total", - "(payouts.net_amount / 100.0)::money::text AS net_total", - "bank_accounts.name AS bank_name", - "payouts.status" - ) + "to_char(payouts.created_at, 'MM/DD/YYYY HH24:MIam') AS date", + '(payouts.gross_amount / 100.0)::money::text AS gross_total', + '(payouts.fee_total / 100.0)::money::text AS fee_total', + '(payouts.net_amount / 100.0)::money::text AS net_total', + 'bank_accounts.name AS bank_name', + 'payouts.status' + ) .from(:payouts) - .join(:bank_accounts, "bank_accounts.nonprofit_id=payouts.nonprofit_id") - .where("payouts.nonprofit_id=$id", id: npo_id) - .and_where("payouts.id=$id", id: payout_id) + .join(:bank_accounts, 'bank_accounts.nonprofit_id=payouts.nonprofit_id') + .where('payouts.nonprofit_id=$id', id: npo_id) + .and_where('payouts.id=$id', id: payout_id) .execute(format: 'csv') .concat([[]]) .concat( Qx.select([ "to_char(payments.date, 'MM/DD/YYYY HH24:MIam') AS \"Date\"", - "(payments.gross_amount/100.0)::money::text AS \"Gross Amount\"", - "(payments.fee_total / 100.0)::money::text AS \"Fee Total\"", - "(payments.net_amount / 100.0)::money::text AS \"Net Amount\"", - "payments.kind AS \"Type\"", - "payments.id AS \"Payment ID\"" - ].concat(QuerySupporters.supporter_export_selections) + '(payments.gross_amount/100.0)::money::text AS "Gross Amount"', + '(payments.fee_total / 100.0)::money::text AS "Fee Total"', + '(payments.net_amount / 100.0)::money::text AS "Net Amount"', + 'payments.kind AS "Type"', + 'payments.id AS "Payment ID"' + ].concat(QuerySupporters.supporter_export_selections) .concat([ - "coalesce(donations.designation, 'None') AS \"Designation\"", - "donations.dedication AS \"Honorarium/Memorium\"", - "donations.anonymous AS \"Anonymous?\"", - "donations.comment AS \"Comment\"", - "coalesce(nullif(campaigns.name, ''), 'None') AS \"Campaign\"", - "coalesce(nullif(campaign_gift_options.name, ''), 'None') AS \"Campaign Gift Level\"", - "coalesce(events.name, 'None') AS \"Event\"" - ]) - ) + "coalesce(donations.designation, 'None') AS \"Designation\"", + 'donations.dedication AS "Honorarium/Memorium"', + 'donations.anonymous AS "Anonymous?"', + 'donations.comment AS "Comment"', + "coalesce(nullif(campaigns.name, ''), 'None') AS \"Campaign\"", + "coalesce(nullif(campaign_gift_options.name, ''), 'None') AS \"Campaign Gift Level\"", + "coalesce(events.name, 'None') AS \"Event\"" + ])) .distinct_on('payments.date, payments.id') .from(:payments) - .join(:payment_payouts, "payment_payouts.payment_id=payments.id") - .add_join(:payouts, "payouts.id=payment_payouts.payout_id") - .left_join(:supporters, "payments.supporter_id=supporters.id") - .add_left_join(:donations, "donations.id=payments.donation_id") - .add_left_join(:campaigns, "donations.campaign_id=campaigns.id") - .add_left_join(:campaign_gifts, "donations.id=campaign_gifts.donation_id") - .add_left_join(:campaign_gift_options, "campaign_gift_options.id=campaign_gifts.campaign_gift_option_id") - .add_left_join(tickets_subquery, "tickets.payment_id=payments.id") - .add_left_join(:events, "events.id=tickets.event_id OR (events.id = donations.event_id)") - .where("payouts.id=$id", id: payout_id) - .and_where("payments.nonprofit_id=$id", id: npo_id) - .order_by("payments.date DESC, payments.id") + .join(:payment_payouts, 'payment_payouts.payment_id=payments.id') + .add_join(:payouts, 'payouts.id=payment_payouts.payout_id') + .left_join(:supporters, 'payments.supporter_id=supporters.id') + .add_left_join(:donations, 'donations.id=payments.donation_id') + .add_left_join(:campaigns, 'donations.campaign_id=campaigns.id') + .add_left_join(:campaign_gifts, 'donations.id=campaign_gifts.donation_id') + .add_left_join(:campaign_gift_options, 'campaign_gift_options.id=campaign_gifts.campaign_gift_option_id') + .add_left_join(tickets_subquery, 'tickets.payment_id=payments.id') + .add_left_join(:events, 'events.id=tickets.event_id OR (events.id = donations.event_id)') + .where('payouts.id=$id', id: payout_id) + .and_where('payments.nonprofit_id=$id', id: npo_id) + .order_by('payments.date DESC, payments.id') .execute(format: 'csv') ) end - def self.find_payments_where_too_far_from_charge_date(id=nil) + def self.find_payments_where_too_far_from_charge_date(id = nil) pay = Payment.includes(:donation).includes(:offsite_payment) - if (id) - pay = pay.where('id = ?', id) - end + pay = pay.where('id = ?', id) if id pay = pay.where('date IS NOT NULL').order('id ASC') - pay.all.each{|p| - next if p.offsite_payment != nil + pay.all.each do |p| + next unless p.offsite_payment.nil? + lowest_charge_for_payment = Charge.where('payment_id = ?', p.id).order('created_at ASC').limit(1).first - - if (lowest_charge_for_payment) + if lowest_charge_for_payment diff = p.date - lowest_charge_for_payment.created_at diff_too_big = diff > 10.minutes || diff < -10.minutes end - if (lowest_charge_for_payment && diff_too_big) + if lowest_charge_for_payment && diff_too_big yield(p.donation.id, p.donation.date, p.id, p.date, lowest_charge_for_payment.id, lowest_charge_for_payment.created_at, diff) end - - } + end end def self.campaign_and_child_query_as_raw_string - "SELECT c_temp.id from campaigns c_temp where c_temp.id=$id OR c_temp.parent_campaign_id=$id" + 'SELECT c_temp.id from campaigns c_temp where c_temp.id=$id OR c_temp.parent_campaign_id=$id' end def self.campaigns_with_creator_email - Qexpr.new.select('campaigns.*, users.email AS creator_email').from(:campaigns).left_outer_join(:profiles, "profiles.id = campaigns.profile_id").left_outer_join(:users, 'users.id = profiles.user_id') + Qexpr.new.select('campaigns.*, users.email AS creator_email').from(:campaigns).left_outer_join(:profiles, 'profiles.id = campaigns.profile_id').left_outer_join(:users, 'users.id = profiles.user_id') end end diff --git a/lib/query/query_profiles.rb b/lib/query/query_profiles.rb index cf3b7e56..d1938a9b 100644 --- a/lib/query/query_profiles.rb +++ b/lib/query/query_profiles.rb @@ -1,28 +1,30 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' module QueryProfiles - def self.for_admin(params) - expr = Qx.select( - 'profiles.name', - 'profiles.id', - 'profiles.created_at::date::text AS created_at', - 'users.confirmed_at AS is_confirmed', - 'users.email') - .from(:profiles) - .add_left_join("users", "profiles.user_id=users.id") - .order_by("profiles.created_at DESC") - .paginate(params[:page].to_i, params[:page_length].to_i) + expr = Qx.select( + 'profiles.name', + 'profiles.id', + 'profiles.created_at::date::text AS created_at', + 'users.confirmed_at AS is_confirmed', + 'users.email' + ) + .from(:profiles) + .add_left_join('users', 'profiles.user_id=users.id') + .order_by('profiles.created_at DESC') + .paginate(params[:page].to_i, params[:page_length].to_i) - if params[:search].present? - expr = expr.where(%Q( - profiles.name LIKE $search - OR users.email LIKE $search - OR users.name LIKE $search - ), search: '%' + params[:search].downcase + '%') - end + if params[:search].present? + expr = expr.where(%( + profiles.name LIKE $search + OR users.email LIKE $search + OR users.name LIKE $search + ), search: '%' + params[:search].downcase + '%') + end - return expr.execute + expr.execute end end diff --git a/lib/query/query_recurring_donations.rb b/lib/query/query_recurring_donations.rb index 8edd9108..8172a0a6 100644 --- a/lib/query/query_recurring_donations.rb +++ b/lib/query/query_recurring_donations.rb @@ -1,33 +1,33 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' require 'psql' module QueryRecurringDonations - # Calculate a nonprofit's total recurring donations def self.monthly_total(np_id) - Qx.select("coalesce(sum(amount), 0) AS sum") - .from("recurring_donations") + Qx.select('coalesce(sum(amount), 0) AS sum') + .from('recurring_donations') .where(nonprofit_id: np_id) .and_where(is_external_active_clause('recurring_donations')) .execute.first['sum'] end - # Fetch a single recurring donation for its edit page def self.fetch_for_edit(id) recurring_donation = Psql.execute( - Qexpr.new.select( - "recurring_donations.*", - "nonprofits.id AS nonprofit_id", - "nonprofits.name AS nonprofit_name", - "cards.name AS card_name" - ).from('recurring_donations') - .left_outer_join('donations', 'donations.id=recurring_donations.donation_id') - .left_outer_join('cards', 'donations.card_id=cards.id') - .left_outer_join('nonprofits', 'nonprofits.id=recurring_donations.nonprofit_id') - .where('recurring_donations.id=$id', id: id) - ).first + Qexpr.new.select( + 'recurring_donations.*', + 'nonprofits.id AS nonprofit_id', + 'nonprofits.name AS nonprofit_name', + 'cards.name AS card_name' + ).from('recurring_donations') + .left_outer_join('donations', 'donations.id=recurring_donations.donation_id') + .left_outer_join('cards', 'donations.card_id=cards.id') + .left_outer_join('nonprofits', 'nonprofits.id=recurring_donations.nonprofit_id') + .where('recurring_donations.id=$id', id: id) + ).first return recurring_donation if !recurring_donation || !recurring_donation['id'] @@ -39,22 +39,21 @@ module QueryRecurringDonations nonprofit = Nonprofit.find(recurring_donation['nonprofit_id']) - return { + { 'recurring_donation' => recurring_donation, 'supporter' => supporter, 'nonprofit' => nonprofit } end - # Construct a full query for the dashboard/export listings def self.full_search_expr(np_id, query) expr = Qexpr.new - .from('recurring_donations') - .left_outer_join('supporters', 'supporters.id=recurring_donations.supporter_id') - .join('donations', 'donations.id=recurring_donations.donation_id') - .left_outer_join('charges paid_charges', 'paid_charges.donation_id=donations.id') - .where('recurring_donations.nonprofit_id=$id', id: np_id.to_i) + .from('recurring_donations') + .left_outer_join('supporters', 'supporters.id=recurring_donations.supporter_id') + .join('donations', 'donations.id=recurring_donations.donation_id') + .left_outer_join('charges paid_charges', 'paid_charges.donation_id=donations.id') + .where('recurring_donations.nonprofit_id=$id', id: np_id.to_i) failed_or_active_clauses = [] @@ -73,29 +72,27 @@ module QueryRecurringDonations failed_or_active_clauses.push("(#{clause})") end - if (failed_or_active_clauses.any?) - expr = expr.where("#{failed_or_active_clauses.join(' OR ')}") + if failed_or_active_clauses.any? + expr = expr.where(failed_or_active_clauses.join(' OR ').to_s) end - - expr = expr.where("paid_charges.id IS NULL OR paid_charges.status != 'failed'") - .group_by('recurring_donations.id') - .order_by('recurring_donations.created_at') + expr = expr.where("paid_charges.id IS NULL OR paid_charges.status != 'failed'") + .group_by('recurring_donations.id') + .order_by('recurring_donations.created_at') if query[:search].present? matcher = "%#{query[:search].downcase.split(' ').join('%')}%" - expr = expr.where(%Q(( + expr = expr.where(%(( lower(supporters.name) LIKE $name OR lower(supporters.email) LIKE $email OR recurring_donations.amount=$amount OR recurring_donations.id=$id - )), {name: matcher, email: matcher, amount: query[:search].to_i, id: query[:search].to_i}) + )), name: matcher, email: matcher, amount: query[:search].to_i, id: query[:search].to_i) end - return expr + expr end - # Fetch the full table of results for the dashboard - def self.full_list(np_id, query={}) + def self.full_list(np_id, query = {}) limit = 30 offset = Qexpr.page_offset(limit, query[:page]) expr = full_search_expr(np_id, query).select( @@ -108,8 +105,9 @@ module QueryRecurringDonations 'MAX(supporters.email) AS email', 'MAX(supporters.name) AS name', 'MAX(supporters.id) AS supporter_id', - 'SUM(paid_charges.amount) AS total_given') - .limit(limit).offset(offset) + 'SUM(paid_charges.amount) AS total_given' + ) + .limit(limit).offset(offset) data = Psql.execute(expr) total_count = Psql.execute( @@ -118,75 +116,74 @@ module QueryRecurringDonations ).first['count'] total_amount = monthly_total(np_id) - return { + { data: data, total_amount: total_amount, total_count: total_count, - remaining: Qexpr.remaining_count(total_count, limit, query[:page]), + remaining: Qexpr.remaining_count(total_count, limit, query[:page]) } end - def self.for_export_enumerable(npo_id, query, chunk_limit=35000) - ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, - query: {required:true, is_hash: true}}) + def self.for_export_enumerable(npo_id, query, chunk_limit = 35_000) + ParamValidation.new({ npo_id: npo_id, query: query }, npo_id: { required: true, is_int: true }, + query: { required: true, is_hash: true }) - return QexprQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| + QexprQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| get_chunk_of_export(npo_id, query, offset, limit, skip_header) end - end - def self.get_chunk_of_export(npo_id, query, offset=nil, limit=nil, skip_header=false ) + def self.get_chunk_of_export(npo_id, query, offset = nil, limit = nil, skip_header = false) root_url = query[:root_url] - return QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) { - full_search_expr(npo_id, query).select( - 'recurring_donations.created_at', - '(recurring_donations.amount / 100.0)::money::text AS amount', - "concat('Every ', recurring_donations.interval, ' ', recurring_donations.time_unit, '(s)') AS interval", - '(SUM(paid_charges.amount) / 100.0)::money::text AS total_contributed', - 'MAX(campaigns.name) AS campaign_name', - 'MAX(supporters.name) AS supporter_name', - 'MAX(supporters.email) AS supporter_email', - 'MAX(supporters.phone) AS phone', - 'MAX(supporters.address) AS address', - 'MAX(supporters.city) AS city', - 'MAX(supporters.state_code) AS state', - 'MAX(supporters.zip_code) AS zip_code', - 'MAX(cards.name) AS card_name', - 'recurring_donations.id AS "Recurring Donation ID"', - 'MAX(donations.id) AS "Donation ID"', - "CASE WHEN #{is_cancelled_clause('recurring_donations')} THEN 'true' ELSE 'false' END AS Cancelled", - "CASE WHEN #{is_failed_clause('recurring_donations')} THEN 'true' ELSE 'false' END AS Failed", - 'recurring_donations.cancelled_at AS "Cancelled At"', - "CASE WHEN #{is_active_clause('recurring_donations')} THEN concat('#{root_url}recurring_donations/', recurring_donations.id, '/edit?t=', recurring_donations.edit_token) ELSE '' END AS \"Donation Management Url\"") - .left_outer_join('campaigns' , 'campaigns.id=donations.campaign_id') - .left_outer_join('cards', 'cards.id=donations.card_id') - } + QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) do + full_search_expr(npo_id, query).select( + 'recurring_donations.created_at', + '(recurring_donations.amount / 100.0)::money::text AS amount', + "concat('Every ', recurring_donations.interval, ' ', recurring_donations.time_unit, '(s)') AS interval", + '(SUM(paid_charges.amount) / 100.0)::money::text AS total_contributed', + 'MAX(campaigns.name) AS campaign_name', + 'MAX(supporters.name) AS supporter_name', + 'MAX(supporters.email) AS supporter_email', + 'MAX(supporters.phone) AS phone', + 'MAX(supporters.address) AS address', + 'MAX(supporters.city) AS city', + 'MAX(supporters.state_code) AS state', + 'MAX(supporters.zip_code) AS zip_code', + 'MAX(cards.name) AS card_name', + 'recurring_donations.id AS "Recurring Donation ID"', + 'MAX(donations.id) AS "Donation ID"', + "CASE WHEN #{is_cancelled_clause('recurring_donations')} THEN 'true' ELSE 'false' END AS Cancelled", + "CASE WHEN #{is_failed_clause('recurring_donations')} THEN 'true' ELSE 'false' END AS Failed", + 'recurring_donations.cancelled_at AS "Cancelled At"', + "CASE WHEN #{is_active_clause('recurring_donations')} THEN concat('#{root_url}recurring_donations/', recurring_donations.id, '/edit?t=', recurring_donations.edit_token) ELSE '' END AS \"Donation Management Url\"" + ) + .left_outer_join('campaigns', 'campaigns.id=donations.campaign_id') + .left_outer_join('cards', 'cards.id=donations.card_id') + end end - def self.recurring_donations_without_cards - RecurringDonation.active.includes(:card).includes(:charges).includes(:donation).includes(:nonprofit).includes(:supporter).where("cards.id IS NULL").order("recurring_donations.created_at DESC") + RecurringDonation.active.includes(:card).includes(:charges).includes(:donation).includes(:nonprofit).includes(:supporter).where('cards.id IS NULL').order('recurring_donations.created_at DESC') end - - def self._splitting_rd_supporters_without_cards() + + def self._splitting_rd_supporters_without_cards supporters_with_valid_rds = [] supporters_without_valid_rds = [] send_to_wendy = [] - supporters_with_cardless_rds = recurring_donations_without_cards.map {|rd| rd.supporter}.uniq{|s| s.id} + supporters_with_cardless_rds = recurring_donations_without_cards.map(&:supporter).uniq(&:id) - #does the supporter have even one rd with a valid card - supporters_with_cardless_rds.each {|s| + # does the supporter have even one rd with a valid card + supporters_with_cardless_rds.each do |s| valid_rd = find_recurring_donation_with_a_card(s) # they have a recurring donation with a card for the same org - if (valid_rd) - if (s.recurring_donations.length > 2) - #they have too many recurring_donations. Send to wendy + if valid_rd + if s.recurring_donations.length > 2 + # they have too many recurring_donations. Send to wendy send_to_wendy.push(s) else # are the recurring_donations the same amount? - if (s.recurring_donations[0].amount == s.recurring_donations[1].amount) + if s.recurring_donations[0].amount == s.recurring_donations[1].amount supporters_with_valid_rds.push(s) else # they're not the same amount. We got no clue. Send to Wendy @@ -197,60 +194,60 @@ module QueryRecurringDonations # they have no other recurring donations supporters_without_valid_rds.push(s) end - } + end - return supporters_with_valid_rds, send_to_wendy, supporters_without_valid_rds + [supporters_with_valid_rds, send_to_wendy, supporters_without_valid_rds] end # @param [Array] wendy_list_of_supporters # @param [String] path - # def self.create_wendy_csv(path, wendy_list_of_supporters) - # CSV.open(path, 'wb') {|csv| - # csv << ['supporter id', 'nonprofit id', 'supporter name', 'supporter address', 'supporter city', 'supporter state', 'supporter ZIP', 'supporter country', 'supporter phone', 'supporter email', 'supporter rd amounts'] - # wendy_list_of_supporters.each { |s| - # amounts = '$'+ s.recurring_donations.active.collect {|rd| Format::Currency.cents_to_dollars(rd.amount)}.join(", $") - # csv << [s.id, s.nonprofit.id, s.name, s.address, s.city, s.state_code, s.zip_code, s.country, s.phone, s.email, amounts] - # } - # } - # end + # def self.create_wendy_csv(path, wendy_list_of_supporters) + # CSV.open(path, 'wb') {|csv| + # csv << ['supporter id', 'nonprofit id', 'supporter name', 'supporter address', 'supporter city', 'supporter state', 'supporter ZIP', 'supporter country', 'supporter phone', 'supporter email', 'supporter rd amounts'] + # wendy_list_of_supporters.each { |s| + # amounts = '$'+ s.recurring_donations.active.collect {|rd| Format::Currency.cents_to_dollars(rd.amount)}.join(", $") + # csv << [s.id, s.nonprofit.id, s.name, s.address, s.city, s.state_code, s.zip_code, s.country, s.phone, s.email, amounts] + # } + # } + # end # @param [Supporter] supporter def self.find_recurring_donation_with_a_card(supporter) - supporter.recurring_donations.select{|rd| - rd.donation != nil && rd.donation.card != nil}.first() + supporter.recurring_donations.select do |rd| + !rd.donation.nil? && !rd.donation.card.nil? + end .first end # Check if a single recdon is due -- used in PayRecurringDonation.with_stripe def self.is_due?(rd_id) Psql.execute( _all_that_are_due - .where("recurring_donations.id=$id", id: rd_id) + .where('recurring_donations.id=$id', id: rd_id) ).any? end - # Sql partial expression # Select all due recurring donations # Can use this for all donations in the db, or extend the query for only those with a nonprofit_id, supporter_id, etc (see is_due?) # XXX horrendous conditional --what is wrong with me? def self._all_that_are_due now = Time.current - Qexpr.new.select("recurring_donations.id") - .from(:recurring_donations) - .where("recurring_donations.active='t'") - .where("coalesce(recurring_donations.n_failures, 0) < 3") - .where("recurring_donations.start_date IS NULL OR recurring_donations.start_date <= $now", now: now) - .where("recurring_donations.end_date IS NULL OR recurring_donations.end_date > $now", now: now) - .join('donations','recurring_donations.donation_id=donations.id and (donations.payment_provider IS NULL OR donations.payment_provider!=\'sepa\')') - .left_outer_join( # Join the most recent paid charge - Qexpr.new.select(:donation_id, "MAX(created_at) AS created_at") - .from(:charges) - .where("status != 'failed'") - .group_by("donation_id") - .as("last_charge"), - "last_charge.donation_id=recurring_donations.donation_id" - ) - .where(%Q( + Qexpr.new.select('recurring_donations.id') + .from(:recurring_donations) + .where("recurring_donations.active='t'") + .where('coalesce(recurring_donations.n_failures, 0) < 3') + .where('recurring_donations.start_date IS NULL OR recurring_donations.start_date <= $now', now: now) + .where('recurring_donations.end_date IS NULL OR recurring_donations.end_date > $now', now: now) + .join('donations', 'recurring_donations.donation_id=donations.id and (donations.payment_provider IS NULL OR donations.payment_provider!=\'sepa\')') + .left_outer_join( # Join the most recent paid charge + Qexpr.new.select(:donation_id, 'MAX(created_at) AS created_at') + .from(:charges) + .where("status != 'failed'") + .group_by('donation_id') + .as('last_charge'), + 'last_charge.donation_id=recurring_donations.donation_id' + ) + .where(%( last_charge.donation_id IS NULL OR ( (recurring_donations.time_unit != 'month' OR recurring_donations.interval != 1) @@ -271,34 +268,34 @@ module QueryRecurringDonations ) ) ) - ), { - now: now, - beginning_of_month: now.beginning_of_month, - beginning_of_last_month: (now - 1.month).beginning_of_month, - today: now.day - }) - .order_by('recurring_donations.created_at') + ), + now: now, + beginning_of_month: now.beginning_of_month, + beginning_of_last_month: (now - 1.month).beginning_of_month, + today: now.day) + .order_by('recurring_donations.created_at') end + # Some general statistics for a nonprofit def self.overall_stats(np_id) - return Psql.execute( + Psql.execute( Qexpr.new.from(:recurring_donations) .select( - "money(avg(recurring_donations.amount) / 100.0) AS average", - "money(coalesce(sum(rds_active.amount), 0) / 100.0) AS active_sum", - "coalesce(count(rds_active), 0) AS active_count", - "money(coalesce(sum(rds_inactive.amount), 0) / 100.0) AS inactive_sum", - "coalesce(count(rds_inactive), 0) AS inactive_count", - "money(coalesce(sum(rds_failed.amount), 0) / 100.0) AS failed_sum", - "coalesce(count(rds_failed), 0) AS failed_count", - "money(coalesce(sum(rds_cancelled.amount), 0) / 100.0) AS cancelled_sum", - "coalesce(count(rds_cancelled), 0) AS cancelled_count" + 'money(avg(recurring_donations.amount) / 100.0) AS average', + 'money(coalesce(sum(rds_active.amount), 0) / 100.0) AS active_sum', + 'coalesce(count(rds_active), 0) AS active_count', + 'money(coalesce(sum(rds_inactive.amount), 0) / 100.0) AS inactive_sum', + 'coalesce(count(rds_inactive), 0) AS inactive_count', + 'money(coalesce(sum(rds_failed.amount), 0) / 100.0) AS failed_sum', + 'coalesce(count(rds_failed), 0) AS failed_count', + 'money(coalesce(sum(rds_cancelled.amount), 0) / 100.0) AS cancelled_sum', + 'coalesce(count(rds_cancelled), 0) AS cancelled_count' ) .left_outer_join('recurring_donations rds_active', "rds_active.id=recurring_donations.id AND #{is_external_active_clause('rds_active')}") .left_outer_join('recurring_donations rds_inactive', "rds_inactive.id=recurring_donations.id AND #{is_external_cancelled_clause('rds_inactive')}") .left_outer_join('recurring_donations rds_failed', "rds_failed.id=recurring_donations.id AND #{is_failed_clause('rds_failed')}") .left_outer_join('recurring_donations rds_cancelled', "rds_cancelled.id=recurring_donations.id AND #{is_cancelled_clause('rds_cancelled')}") - .where("recurring_donations.nonprofit_id=$id", id: np_id) + .where('recurring_donations.nonprofit_id=$id', id: np_id) ).first end @@ -316,7 +313,6 @@ module QueryRecurringDonations "#{field_for_rd}.active='t'" end - def self.is_cancelled_clause(field_for_rd) "NOT (#{is_active_clause(field_for_rd)})" end @@ -330,21 +326,23 @@ module QueryRecurringDonations end def self.last_charge - Qexpr.new.select(:donation_id, "MAX(created_at) AS created_at") - .from(:charges) - .where("status != 'failed'") - .group_by("donation_id") - .as("last_charge") + Qexpr.new.select(:donation_id, 'MAX(created_at) AS created_at') + .from(:charges) + .where("status != 'failed'") + .group_by('donation_id') + .as('last_charge') end def self.export_for_transfer(nonprofit_id) - items = RecurringDonation.where("nonprofit_id = ?", nonprofit_id).active.includes('supporter').includes('card').to_a - output = items.map{|i| {supporter: i.supporter.id, - supporter_name: i.supporter.name, - supporter_email: i.supporter.email, - amount: i.amount, - paydate: i.paydate, - card: i.card.stripe_card_id}} - return output.to_a + items = RecurringDonation.where('nonprofit_id = ?', nonprofit_id).active.includes('supporter').includes('card').to_a + output = items.map do |i| + { supporter: i.supporter.id, + supporter_name: i.supporter.name, + supporter_email: i.supporter.email, + amount: i.amount, + paydate: i.paydate, + card: i.card.stripe_card_id } + end + output.to_a end end diff --git a/lib/query/query_roles.rb b/lib/query/query_roles.rb index 01db6251..b7807784 100644 --- a/lib/query/query_roles.rb +++ b/lib/query/query_roles.rb @@ -1,27 +1,28 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryRoles + def self.user_has_role?(user_id, role_names, host_id = nil) + expr = Qx.select('COUNT(roles)').from(:roles) + .where('name IN ($names)', names: Array(role_names)) + .and_where(user_id: user_id) + expr = expr.and_where(host_id: host_id) if host_id + expr.execute.first['count'] > 0 + end - def self.user_has_role?(user_id, role_names, host_id=nil) - expr = Qx.select("COUNT(roles)").from(:roles) - .where("name IN ($names)", names: Array(role_names)) - .and_where(user_id: user_id) - expr = expr.and_where(host_id: host_id) if host_id - return expr.execute.first['count'] > 0 - end + # Get host tables -- host can be nonprofit, campaign, event + def self.host_ids(user_id, role_names) + Qx.select('host_id').from(:roles) + .where(user_id: user_id) + .and_where('roles.name IN ($names)', names: role_names) + .execute.map { |h| h['host_id'] } + end - # Get host tables -- host can be nonprofit, campaign, event - def self.host_ids(user_id, role_names) - Qx.select("host_id").from(:roles) - .where(user_id: user_id) - .and_where("roles.name IN ($names)", names: role_names) - .execute.map{|h| h['host_id']} - end + def self.is_nonprofit_user?(user_id, np_id) + user_has_role?(user_id, %i[nonprofit_admin nonprofit_associate], np_id) + end - def self.is_nonprofit_user?(user_id, np_id) - user_has_role?(user_id, [:nonprofit_admin, :nonprofit_associate], np_id) - end - - def self.is_authorized_for_nonprofit?(user_id, np_id) - user_has_role?(user_id, [:super_admin]) || is_nonprofit_user?(user_id, np_id) - end -end \ No newline at end of file + def self.is_authorized_for_nonprofit?(user_id, np_id) + user_has_role?(user_id, [:super_admin]) || is_nonprofit_user?(user_id, np_id) + end +end diff --git a/lib/query/query_source_token.rb b/lib/query/query_source_token.rb index 25a7abb6..0c9233ec 100644 --- a/lib/query/query_source_token.rb +++ b/lib/query/query_source_token.rb @@ -1,6 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QuerySourceToken - EXPIRED_TOKEN_MESSAGE = "There was an error processing your card and it was not charged. Please try again." + EXPIRED_TOKEN_MESSAGE = 'There was an error processing your card and it was not charged. Please try again.' AUTH_ERROR_MESSAGE = "You're not authorized to make this charge" # @param [String] source_token @@ -11,43 +13,36 @@ module QuerySourceToken # @raise [ExpiredTokenError] when the source token has already been used too many times # or we're past the expiration date def self.get_and_increment_source_token(token, user = nil) - ParamValidation.new({token: token}, { - token: {required: true, format: UUID::Regex} - }) + ParamValidation.new({ token: token }, + token: { required: true, format: UUID::Regex }) source_token = SourceToken.where('token = ?', token).first if source_token - source_token.with_lock { + source_token.with_lock do unless source_token_unexpired?(source_token) - raise ExpiredTokenError.new(EXPIRED_TOKEN_MESSAGE) + raise ExpiredTokenError, EXPIRED_TOKEN_MESSAGE end if source_token.event - unless user - raise AuthenticationError.new AUTH_ERROR_MESSAGE - end + raise AuthenticationError, AUTH_ERROR_MESSAGE unless user unless QueryRoles.is_authorized_for_nonprofit?(user.id, source_token.event.nonprofit.id) - raise AuthenticationError.new AUTH_ERROR_MESSAGE + raise AuthenticationError, AUTH_ERROR_MESSAGE end end source_token.total_uses = source_token.total_uses + 1 source_token.save! - } + end else - raise ParamValidation::ValidationError.new "#{token} doesn't represent a valid source", {:key => :token} + raise ParamValidation::ValidationError.new "#{token} doesn't represent a valid source", key: :token end source_token end - def self.source_token_unexpired?(source_token) - if source_token.max_uses <= source_token.total_uses - return false - end - if source_token.expiration < Time.now - return false - end + return false if source_token.max_uses <= source_token.total_uses + return false if source_token.expiration < Time.now + true end @@ -57,4 +52,4 @@ module QuerySourceToken raise ParamValidation::ValidationError.new("The item for token #{data[:token]} is not a Card", key: :token) end end -end \ No newline at end of file +end diff --git a/lib/query/query_supporters.rb b/lib/query/query_supporters.rb index 017a9948..eab913da 100644 --- a/lib/query/query_supporters.rb +++ b/lib/query/query_supporters.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qexpr' require 'psql' @@ -6,44 +8,42 @@ require 'format/currency' require 'format/csv' module QuerySupporters - # Query supporters and their donations and gift levels for a campaign def self.campaign_list_expr(np_id, campaign_id, query) expr = Qexpr.new.from('supporters') - .left_outer_join('donations', 'donations.supporter_id=supporters.id') - .left_outer_join('campaign_gifts', 'donations.id=campaign_gifts.donation_id') - .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') - .join_lateral(:payments, Qx + .left_outer_join('donations', 'donations.supporter_id=supporters.id') + .left_outer_join('campaign_gifts', 'donations.id=campaign_gifts.donation_id') + .left_outer_join('campaign_gift_options', 'campaign_gifts.campaign_gift_option_id=campaign_gift_options.id') + .join_lateral(:payments, Qx .select('payments.id, payments.gross_amount').from(:payments) .where('payments.donation_id = donations.id') .order_by('payments.created_at ASC') .limit(1).parse, true) - .join(Qx.select('id, profile_id').from('campaigns') + .join(Qx.select('id, profile_id').from('campaigns') .where("id IN (#{QueryCampaigns .get_campaign_and_children(campaign_id) .parse})").as('campaigns').parse, - 'donations.campaign_id=campaigns.id') - .join(Qx.select('users.id, profiles.id AS profiles_id, users.email') + 'donations.campaign_id=campaigns.id') + .join(Qx.select('users.id, profiles.id AS profiles_id, users.email') .from('users') .add_join('profiles', 'profiles.user_id = users.id') - .as("users").parse, "users.profiles_id=campaigns.profile_id") - .where("supporters.nonprofit_id=$id", id: np_id) - .group_by('supporters.id') - .order_by('MAX(donations.date) DESC') + .as('users').parse, 'users.profiles_id=campaigns.profile_id') + .where('supporters.nonprofit_id=$id', id: np_id) + .group_by('supporters.id') + .order_by('MAX(donations.date) DESC') - if query[:search].present? - expr = expr.where(%Q( + if query[:search].present? + expr = expr.where(%( supporters.name ILIKE $search OR supporters.email ILIKE $search OR campaign_gift_options.name ILIKE $search ), search: '%' + query[:search] + '%') end - return expr + expr end - - # Used in the campaign donor listing - def self.campaign_list(np_id, campaign_id, query) + # Used in the campaign donor listing + def self.campaign_list(np_id, campaign_id, query) limit = 50 offset = Qexpr.page_offset(limit, query[:page]) @@ -60,24 +60,24 @@ module QuerySupporters ) total_count = Psql.execute( - Qexpr.new.select("COUNT(s)") + Qexpr.new.select('COUNT(s)') .from(campaign_list_expr(np_id, campaign_id, query).remove(:order_by).select('supporters.id').as('s').parse) ).first['count'] - return { - data: data, - total_count: total_count, - remaining: Qexpr.remaining_count(total_count, limit, query[:page]) + { + data: data, + total_count: total_count, + remaining: Qexpr.remaining_count(total_count, limit, query[:page]) } - end + end def self.full_search_metrics(np_id, query) total_count = full_filter_expr(np_id, query) - .select("COUNT(supporters)") - .remove_clause(:order_by) - .execute.first['count'] + .select('COUNT(supporters)') + .remove_clause(:order_by) + .execute.first['count'] - return { + { total_count: total_count, remaining_count: Qexpr.remaining_count(total_count, 30, query[:page]) } @@ -94,40 +94,34 @@ module QuerySupporters "to_char(payments.max_date, 'MM/DD/YY') AS last_contribution", 'payments.sum AS total_raised' ] - if query[:select] - select += query[:select].split(',') - end + select += query[:select].split(',') if query[:select] supps = full_filter_expr(np_id, query) - .select(*select) - .paginate(query[:page].to_i, 30) - .execute + .select(*select) + .paginate(query[:page].to_i, 30) + .execute - return { data: supps } + { data: supps } end - - def self._full_search(np_id, query) select = [ - 'supporters.name', - 'supporters.email', - 'supporters.is_unsubscribed_from_emails', - 'supporters.id AS id', - 'tags.names AS tags', - "to_char(payments.max_date, 'MM/DD/YY') AS last_contribution", - 'payments.sum AS total_raised' + 'supporters.name', + 'supporters.email', + 'supporters.is_unsubscribed_from_emails', + 'supporters.id AS id', + 'tags.names AS tags', + "to_char(payments.max_date, 'MM/DD/YY') AS last_contribution", + 'payments.sum AS total_raised' ] - if query[:select] - select += query[:select].split(',') - end + select += query[:select].split(',') if query[:select] supps = full_filter_expr(np_id, query) - .select(*select) - .paginate(query[:page].to_i, query[:page_length].to_i) - .execute + .select(*select) + .paginate(query[:page].to_i, query[:page_length].to_i) + .execute - return { data: supps } + { data: supps } end # # Given a list of supporters, you may want to remove duplicates from those supporters. @@ -154,74 +148,73 @@ module QuerySupporters # return new_supporters # end - # Perform all filters and search for /nonprofits/id/supporters dashboard and export def self.full_filter_expr(np_id, query) payments_subquery = - Qx.select("supporter_id", "SUM(gross_amount)", "MAX(date) AS max_date", "MIN(date) AS min_date", "COUNT(*) AS count") - .from( - Qx.select("supporter_id", "date", "gross_amount") - .from(:payments) - .join(Qx.select('id') - .from(:supporters) - .where("supporters.nonprofit_id = $id and deleted != 'true'", id: np_id ) - .as("payments_to_supporters"), "payments_to_supporters.id = payments.supporter_id" - ) - .as("outer_from_payment_to_supporter") - .parse) - .group_by(:supporter_id) - .as(:payments) + Qx.select('supporter_id', 'SUM(gross_amount)', 'MAX(date) AS max_date', 'MIN(date) AS min_date', 'COUNT(*) AS count') + .from( + Qx.select('supporter_id', 'date', 'gross_amount') + .from(:payments) + .join(Qx.select('id') + .from(:supporters) + .where("supporters.nonprofit_id = $id and deleted != 'true'", id: np_id) + .as('payments_to_supporters'), 'payments_to_supporters.id = payments.supporter_id') + .as('outer_from_payment_to_supporter') + .parse + ) + .group_by(:supporter_id) + .as(:payments) - tags_subquery = Qx.select("tag_joins.supporter_id", "ARRAY_AGG(tag_masters.id) AS ids", "ARRAY_AGG(tag_masters.name::text) AS names") - .from(:tag_joins) - .join(:tag_masters, "tag_masters.id=tag_joins.tag_master_id") - .where("tag_masters.deleted IS NULL") - .group_by("tag_joins.supporter_id") - .as(:tags) + tags_subquery = Qx.select('tag_joins.supporter_id', 'ARRAY_AGG(tag_masters.id) AS ids', 'ARRAY_AGG(tag_masters.name::text) AS names') + .from(:tag_joins) + .join(:tag_masters, 'tag_masters.id=tag_joins.tag_master_id') + .where('tag_masters.deleted IS NULL') + .group_by('tag_joins.supporter_id') + .as(:tags) expr = Qx.select('supporters.id').from(:supporters) - .where( - ["supporters.nonprofit_id=$id", id: np_id.to_i], - ["supporters.deleted != true"] - ) - .left_join( - [tags_subquery, "tags.supporter_id=supporters.id"], - [payments_subquery, "payments.supporter_id=supporters.id"] - ) - .order_by('payments.max_date DESC NULLS LAST') + .where( + ['supporters.nonprofit_id=$id', id: np_id.to_i], + ['supporters.deleted != true'] + ) + .left_join( + [tags_subquery, 'tags.supporter_id=supporters.id'], + [payments_subquery, 'payments.supporter_id=supporters.id'] + ) + .order_by('payments.max_date DESC NULLS LAST') if query[:last_payment_after].present? - expr = expr.and_where("payments.max_date > $d", d: Chronic.parse(query[:last_payment_after])) + expr = expr.and_where('payments.max_date > $d', d: Chronic.parse(query[:last_payment_after])) end if query[:last_payment_before].present? - expr = expr.and_where("payments.max_date < $d", d: Chronic.parse(query[:last_payment_before])) + expr = expr.and_where('payments.max_date < $d', d: Chronic.parse(query[:last_payment_before])) end if query[:first_payment_after].present? - expr = expr.and_where("payments.min_date > $d", d: Chronic.parse(query[:first_payment_after])) + expr = expr.and_where('payments.min_date > $d', d: Chronic.parse(query[:first_payment_after])) end if query[:first_payment_before].present? - expr = expr.and_where("payments.min_date < $d", d: Chronic.parse(query[:first_payment_before])) + expr = expr.and_where('payments.min_date < $d', d: Chronic.parse(query[:first_payment_before])) end if query[:total_raised_greater_than].present? - expr = expr.and_where("payments.sum > $amount", amount: query[:total_raised_greater_than].to_i * 100) + expr = expr.and_where('payments.sum > $amount', amount: query[:total_raised_greater_than].to_i * 100) end if query[:total_raised_less_than].present? - expr = expr.and_where("payments.sum < $amount OR payments.supporter_id IS NULL", amount: query[:total_raised_less_than].to_i * 100) + expr = expr.and_where('payments.sum < $amount OR payments.supporter_id IS NULL', amount: query[:total_raised_less_than].to_i * 100) end - if ['week', 'month', 'quarter', 'year'].include? query[:has_contributed_during] + if %w[week month quarter year].include? query[:has_contributed_during] d = Time.current.send('beginning_of_' + query[:has_contributed_during]) - expr = expr.and_where("payments.max_date >= $d", d: d) + expr = expr.and_where('payments.max_date >= $d', d: d) end - if ['week', 'month', 'quarter', 'year'].include? query[:has_not_contributed_during] + if %w[week month quarter year].include? query[:has_not_contributed_during] d = Time.current.send('beginning_of_' + query[:has_not_contributed_during]) - expr = expr.and_where("payments.count = 0 OR payments.max_date <= $d", d: d) + expr = expr.and_where('payments.count = 0 OR payments.max_date <= $d', d: d) end if query[:MAX_payment_before].present? date_ago = Timespan::TimeUnits[query[:MAX_payment_before]].utc - expr = expr.and_where("payments.max_date < $date OR payments.count = 0", date: date_ago) + expr = expr.and_where('payments.max_date < $date OR payments.count = 0', date: date_ago) end if query[:search].present? - expr = expr.and_where(%Q( + expr = expr.and_where(%( supporters.name ILIKE $search OR supporters.email ILIKE $search OR supporters.organization ILIKE $search @@ -229,65 +222,66 @@ module QuerySupporters end if query[:notes].present? notes_subquery = Qx.select("STRING_AGG(content, ' ') as content, supporter_id") - .from(:supporter_notes) - .group_by(:supporter_id) - .as(:notes) - expr = expr.add_left_join(notes_subquery, "notes.supporter_id=supporters.id") - .and_where("to_tsvector('english', notes.content) @@ plainto_tsquery('english', $notes)", notes: query[:notes]) + .from(:supporter_notes) + .group_by(:supporter_id) + .as(:notes) + expr = expr.add_left_join(notes_subquery, 'notes.supporter_id=supporters.id') + .and_where("to_tsvector('english', notes.content) @@ plainto_tsquery('english', $notes)", notes: query[:notes]) end if query[:custom_fields].present? - c_f_subquery = Qx.select("STRING_AGG(value, ' ') as value", "supporter_id") - .from(:custom_field_joins) - .group_by("custom_field_joins.supporter_id") - .as(:custom_fields) - expr = expr.add_left_join(c_f_subquery, "custom_fields.supporter_id=supporters.id") - .and_where("to_tsvector('english', custom_fields.value) @@ plainto_tsquery('english', $custom_fields)", custom_fields: query[:custom_fields]) + c_f_subquery = Qx.select("STRING_AGG(value, ' ') as value", 'supporter_id') + .from(:custom_field_joins) + .group_by('custom_field_joins.supporter_id') + .as(:custom_fields) + expr = expr.add_left_join(c_f_subquery, 'custom_fields.supporter_id=supporters.id') + .and_where("to_tsvector('english', custom_fields.value) @@ plainto_tsquery('english', $custom_fields)", custom_fields: query[:custom_fields]) end if query[:location].present? - expr = expr.and_where("lower(supporters.city) LIKE $city OR lower(supporters.zip_code) LIKE $zip", city: query[:location].downcase, zip: query[:location].downcase) + expr = expr.and_where('lower(supporters.city) LIKE $city OR lower(supporters.zip_code) LIKE $zip', city: query[:location].downcase, zip: query[:location].downcase) end if query[:recurring].present? - rec_ps_subquery = Qx.select("payments.count", "payments.supporter_id") - .from(:payments) - .where("kind='RecurringDonation'") - .group_by("payments.supporter_id") - .as(:rec_ps) - expr = expr.add_left_join(rec_ps_subquery, "rec_ps.supporter_id=supporters.id") - .and_where('rec_ps.count > 0') + rec_ps_subquery = Qx.select('payments.count', 'payments.supporter_id') + .from(:payments) + .where("kind='RecurringDonation'") + .group_by('payments.supporter_id') + .as(:rec_ps) + expr = expr.add_left_join(rec_ps_subquery, 'rec_ps.supporter_id=supporters.id') + .and_where('rec_ps.count > 0') end if query[:ids].present? - expr = expr.and_where("supporters.id IN ($ids)", ids: query[:ids].split(",").map(&:to_i)) + expr = expr.and_where('supporters.id IN ($ids)', ids: query[:ids].split(',').map(&:to_i)) end if query[:select].present? - expr = expr.select(*query[:select].split(",").map{|x| Qx.quote_ident(x)}) + expr = expr.select(*query[:select].split(',').map { |x| Qx.quote_ident(x) }) end # Sort by supporters who have all of the list of tag names if query[:tags].present? tag_ids = (query[:tags].is_a?(String) ? query[:tags].split(',') : query[:tags]).map(&:to_i) - expr = expr.and_where("tags.ids @> ARRAY[$tag_ids]", tag_ids: tag_ids) + expr = expr.and_where('tags.ids @> ARRAY[$tag_ids]', tag_ids: tag_ids) end if query[:campaign_id].present? - expr = expr.add_join("donations", "donations.supporter_id=supporters.id AND donations.campaign_id IN (#{QueryCampaigns + expr = expr.add_join('donations', "donations.supporter_id=supporters.id AND donations.campaign_id IN (#{QueryCampaigns .get_campaign_and_children(query[:campaign_id].to_i) .parse})") end if query[:event_id].present? - select_tickets_supporters = Qx.select("event_ticket_supporters.supporter_id") - .from( - "#{Qx.select("MAX(tickets.event_id) AS event_id", "tickets.supporter_id") - .from(:tickets) - .where("event_id = $event_id", event_id: query[:event_id]) - .group_by(:supporter_id).as('event_ticket_supporters').parse}" - ) + select_tickets_supporters = Qx.select('event_ticket_supporters.supporter_id') + .from( + Qx.select('MAX(tickets.event_id) AS event_id', 'tickets.supporter_id') + .from(:tickets) + .where('event_id = $event_id', event_id: query[:event_id]) + .group_by(:supporter_id).as('event_ticket_supporters').parse.to_s + ) select_donation_supporters = - Qx.select("event_donation_supporters.supporter_id") - .from( - "#{Qx.select("MAX(donations.event_id) AS event_id", "donations.supporter_id") - .from(:donations) - .where("event_id = $event_id", event_id: query[:event_id] ) - .group_by(:supporter_id).as('event_donation_supporters').parse}") + Qx.select('event_donation_supporters.supporter_id') + .from( + Qx.select('MAX(donations.event_id) AS event_id', 'donations.supporter_id') + .from(:donations) + .where('event_id = $event_id', event_id: query[:event_id]) + .group_by(:supporter_id).as('event_donation_supporters').parse.to_s + ) union_expr = "( #{select_tickets_supporters.parse} @@ -296,39 +290,38 @@ UNION DISTINCT ) AS event_supporters" expr = expr - .add_join( - union_expr, - "event_supporters.supporter_id=supporters.id" - ) + .add_join( + union_expr, + 'event_supporters.supporter_id=supporters.id' + ) end - if ['asc', 'desc'].include? query[:sort_name] - expr = expr.order_by(["supporters.name", query[:sort_name]]) + if %w[asc desc].include? query[:sort_name] + expr = expr.order_by(['supporters.name', query[:sort_name]]) end - if ['asc', 'desc'].include? query[:sort_contributed] - expr = expr.and_where("payments.sum > 0").order_by(["payments.sum", query[:sort_contributed]]) + if %w[asc desc].include? query[:sort_contributed] + expr = expr.and_where('payments.sum > 0').order_by(['payments.sum', query[:sort_contributed]]) end - if ['asc', 'desc'].include? query[:sort_last_payment] - expr = expr.order_by(["payments.max_date", "#{query[:sort_last_payment].upcase} NULLS LAST"]) + if %w[asc desc].include? query[:sort_last_payment] + expr = expr.order_by(['payments.max_date', "#{query[:sort_last_payment].upcase} NULLS LAST"]) end - return expr + expr end - def self.for_export_enumerable(npo_id, query, chunk_limit=35000) - ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, - query: {required:true, is_hash: true}}) + def self.for_export_enumerable(npo_id, query, chunk_limit = 35_000) + ParamValidation.new({ npo_id: npo_id, query: query }, npo_id: { required: true, is_int: true }, + query: { required: true, is_hash: true }) - return QxQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| + QxQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| get_chunk_of_export(npo_id, query, offset, limit, skip_header) end - end - def self.get_chunk_of_export(np_id, query, offset=nil, limit=nil, skip_header=false) - return QxQueryChunker.get_chunk_of_query(offset, limit, skip_header) do + def self.get_chunk_of_export(np_id, query, offset = nil, limit = nil, skip_header = false) + QxQueryChunker.get_chunk_of_query(offset, limit, skip_header) do expr = full_filter_expr(np_id, query) selects = supporter_export_selections.concat([ - '(payments.sum / 100)::money::text AS total_contributed', - 'supporters.id AS id' + '(payments.sum / 100)::money::text AS total_contributed', + 'supporters.id AS id' ]) if query[:export_custom_fields] # Add a select/csv-column for every custom field master for this nonprofit @@ -340,57 +333,53 @@ UNION DISTINCT # ... ids = query[:export_custom_fields].split(',').map(&:to_i) if ids.any? - cfms = Qx.select("name", "id").from(:custom_field_masters).where(nonprofit_id: np_id).and_where("id IN ($ids)", ids: ids).ex + cfms = Qx.select('name', 'id').from(:custom_field_masters).where(nonprofit_id: np_id).and_where('id IN ($ids)', ids: ids).ex cfms.compact.map do |cfm| - table_alias = "cfjs_#{cfm['name'].gsub(/\$/, "")}" + table_alias = "cfjs_#{cfm['name'].delete('$')}" table_alias_quot = "\"#{table_alias}\"" - field_join_subq = Qx.select("STRING_AGG(value, ',') as value", "supporter_id") - .from("custom_field_joins") - .join("custom_field_masters" , "custom_field_masters.id=custom_field_joins.custom_field_master_id") - .where("custom_field_masters.id=$id", id: cfm['id']) - .group_by(:supporter_id) - .as(table_alias) + field_join_subq = Qx.select("STRING_AGG(value, ',') as value", 'supporter_id') + .from('custom_field_joins') + .join('custom_field_masters', 'custom_field_masters.id=custom_field_joins.custom_field_master_id') + .where('custom_field_masters.id=$id', id: cfm['id']) + .group_by(:supporter_id) + .as(table_alias) expr.add_left_join(field_join_subq, "#{table_alias_quot}.supporter_id=supporters.id") selects = selects.concat(["#{table_alias_quot}.value AS \"#{cfm['name']}\""]) end end end - - get_last_payment_query = Qx.select('supporter_id', "MAX(date) AS date") - .from(:payments) - .group_by("supporter_id") - .as("last_payment") + get_last_payment_query = Qx.select('supporter_id', 'MAX(date) AS date') + .from(:payments) + .group_by('supporter_id') + .as('last_payment') expr.add_left_join(get_last_payment_query, 'last_payment.supporter_id = supporters.id') selects = selects.concat(['last_payment.date as "Last Payment Received"']) - - supporter_note_query = Qx.select("STRING_AGG(supporter_notes.created_at || ': ' || supporter_notes.content, '\r\n' ORDER BY supporter_notes.created_at DESC) as notes", "supporter_notes.supporter_id") - .from(:supporter_notes) - .group_by('supporter_notes.supporter_id') - .as("supporter_note_query") + supporter_note_query = Qx.select("STRING_AGG(supporter_notes.created_at || ': ' || supporter_notes.content, '\r\n' ORDER BY supporter_notes.created_at DESC) as notes", 'supporter_notes.supporter_id') + .from(:supporter_notes) + .group_by('supporter_notes.supporter_id') + .as('supporter_note_query') expr.add_left_join(supporter_note_query, 'supporter_note_query.supporter_id=supporters.id') - selects = selects.concat(["supporter_note_query.notes AS notes"]).concat(["ARRAY_TO_STRING(tags.names, ',') as tags"]) - + selects = selects.concat(['supporter_note_query.notes AS notes']).concat(["ARRAY_TO_STRING(tags.names, ',') as tags"]) expr.select(selects) end end - def self.supporter_note_export_enumerable(npo_id, query, chunk_limit=35000) - ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, - query: {required:true, is_hash: true}}) + def self.supporter_note_export_enumerable(npo_id, query, chunk_limit = 35_000) + ParamValidation.new({ npo_id: npo_id, query: query }, npo_id: { required: true, is_int: true }, + query: { required: true, is_hash: true }) - return QxQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| + QxQueryChunker.for_export_enumerable(chunk_limit) do |offset, limit, skip_header| get_chunk_of_supporter_note_export(npo_id, query, offset, limit, skip_header) end - end - def self.get_chunk_of_supporter_note_export(np_id, query, offset=nil, limit=nil, skip_header=false) - return QxQueryChunker.get_chunk_of_query(offset, limit, skip_header) do + def self.get_chunk_of_supporter_note_export(np_id, query, offset = nil, limit = nil, skip_header = false) + QxQueryChunker.get_chunk_of_query(offset, limit, skip_header) do expr = full_filter_expr(np_id, query) supporter_note_select = [ 'supporters.id', @@ -408,41 +397,41 @@ UNION DISTINCT def self.for_export(np_id, query) expr = full_filter_expr(np_id, query) selects = supporter_export_selections.concat([ - '(payments.sum / 100)::money::text AS total_contributed', - 'supporters.id AS id' - ]) + '(payments.sum / 100)::money::text AS total_contributed', + 'supporters.id AS id' + ]) if query[:export_custom_fields] # Add a select/csv-column for every custom field master for this nonprofit # and add a left join for every custom field master - # eg if the npo has a custom field like Employer with id 99, then the query will be - # SELECT export_cfj_Employer.value AS Employer, ... - # FROM supporters + # eg if the npo has a custom field like Employer with id 99, then the query will be + # SELECT export_cfj_Employer.value AS Employer, ... + # FROM supporters # LEFT JOIN custom_field_joins AS export_cfj_Employer ON export_cfj_Employer.supporter_id=supporters.id AND export_cfj_Employer.custom_field_master_id=99 # ... ids = query[:export_custom_fields].split(',').map(&:to_i) if ids.any? - cfms = Qx.select("name", "id").from(:custom_field_masters).where(nonprofit_id: np_id).and_where("id IN ($ids)", ids: ids).ex + cfms = Qx.select('name', 'id').from(:custom_field_masters).where(nonprofit_id: np_id).and_where('id IN ($ids)', ids: ids).ex cfms.compact.map do |cfm| - table_alias = "cfjs_#{cfm['name'].gsub(/\$/, "")}" + table_alias = "cfjs_#{cfm['name'].delete('$')}" table_alias_quot = "\"#{table_alias}\"" - field_join_subq = Qx.select("STRING_AGG(value, ',') as value", "supporter_id") - .from("custom_field_joins") - .join("custom_field_masters" , "custom_field_masters.id=custom_field_joins.custom_field_master_id") - .where("custom_field_masters.id=$id", id: cfm['id']) - .group_by(:supporter_id) - .as(table_alias) + field_join_subq = Qx.select("STRING_AGG(value, ',') as value", 'supporter_id') + .from('custom_field_joins') + .join('custom_field_masters', 'custom_field_masters.id=custom_field_joins.custom_field_master_id') + .where('custom_field_masters.id=$id', id: cfm['id']) + .group_by(:supporter_id) + .as(table_alias) expr.add_left_join(field_join_subq, "#{table_alias_quot}.supporter_id=supporters.id") selects = selects.concat(["#{table_alias_quot}.value AS \"#{cfm['name']}\""]) end end end - supporter_note_query = Qx.select("STRING_AGG(supporter_notes.created_at || ': ' || supporter_notes.content, '\r\n' ORDER BY supporter_notes.created_at DESC) as notes", "supporter_notes.supporter_id") - .from(:supporter_notes) - .group_by('supporter_notes.supporter_id') - .as("supporter_note_query") + supporter_note_query = Qx.select("STRING_AGG(supporter_notes.created_at || ': ' || supporter_notes.content, '\r\n' ORDER BY supporter_notes.created_at DESC) as notes", 'supporter_notes.supporter_id') + .from(:supporter_notes) + .group_by('supporter_notes.supporter_id') + .as('supporter_note_query') expr.add_left_join(supporter_note_query, 'supporter_note_query.supporter_id=supporters.id') - selects = selects.concat(["supporter_note_query.notes AS notes"]) + selects = selects.concat(['supporter_note_query.notes AS notes']) expr.select(selects).execute(format: 'csv') end @@ -451,17 +440,17 @@ UNION DISTINCT [ "substring(trim(both from supporters.name) from '^.+ ([^\s]+)$') AS \"Last Name\"", "substring(trim(both from supporters.name) from '^(.+) [^\s]+$') AS \"First Name\"", - "trim(both from supporters.name) AS \"Full Name\"", - "supporters.organization AS \"Organization\"", - "supporters.email \"Email\"", - "supporters.phone \"Phone\"", - "supporters.address \"Address\"", - "supporters.city \"City\"", - "supporters.state_code \"State\"", - "supporters.zip_code \"Postal Code\"", - "supporters.country \"Country\"", - "supporters.anonymous \"Anonymous?\"", - "supporters.id \"Supporter ID\"" + 'trim(both from supporters.name) AS "Full Name"', + 'supporters.organization AS "Organization"', + 'supporters.email "Email"', + 'supporters.phone "Phone"', + 'supporters.address "Address"', + 'supporters.city "City"', + 'supporters.state_code "State"', + 'supporters.zip_code "Postal Code"', + 'supporters.country "Country"', + 'supporters.anonymous "Anonymous?"', + 'supporters.id "Supporter ID"' ] end @@ -469,9 +458,9 @@ UNION DISTINCT # Partial sql expression def self.dupes_expr(np_id) - Qx.select("ARRAY_AGG(id) AS ids") + Qx.select('ARRAY_AGG(id) AS ids') .from(:supporters) - .where("nonprofit_id=$id", id: np_id) + .where('nonprofit_id=$id', id: np_id) .and_where("deleted='f' OR deleted IS NULL") .having('COUNT(id) > 1') end @@ -483,7 +472,7 @@ UNION DISTINCT # (each sub-array is a group of duplicates) def self.dupes_on_email(np_id) dupes_expr(np_id) - .and_where("email IS NOT NULL") + .and_where('email IS NOT NULL') .and_where("email != ''") .group_by(:email) .execute(format: 'csv')[1..-1] @@ -493,7 +482,7 @@ UNION DISTINCT # Find all duplicate supporters by the name column def self.dupes_on_name(np_id) dupes_expr(np_id) - .and_where("name IS NOT NULL") + .and_where('name IS NOT NULL') .group_by(:name) .execute(format: 'csv')[1..-1] .map(&:flatten) @@ -504,7 +493,7 @@ UNION DISTINCT def self.dupes_on_name_and_email(np_id) dupes_expr(np_id) .and_where("name IS NOT NULL AND email IS NOT NULL AND email != ''") - .group_by("name, email") + .group_by('name, email') .execute(format: 'csv')[1..-1] .map(&:flatten) end @@ -514,22 +503,22 @@ UNION DISTINCT # Only including payments for the given year def self.end_of_year_donor_report(np_id, year) supporter_expr = Qexpr.new - .select( supporter_export_selections.concat(["(payments.sum / 100.0)::money::text AS \"Total Contributions #{year}\"", "supporters.id"]) ) - .from(:supporters) - .join(Qexpr.new - .select("SUM(gross_amount)", "supporter_id") + .select(supporter_export_selections.concat(["(payments.sum / 100.0)::money::text AS \"Total Contributions #{year}\"", 'supporters.id'])) + .from(:supporters) + .join(Qexpr.new + .select('SUM(gross_amount)', 'supporter_id') .from(:payments) .group_by(:supporter_id) - .where("date >= $date", date: "#{year}-01-01 00:00:00 UTC") - .where("date < $date", date: "#{year+1}-01-01 00:00:00 UTC") - .as(:payments), "payments.supporter_id=supporters.id") - .where('payments.sum > 25000') - .as('supporters') + .where('date >= $date', date: "#{year}-01-01 00:00:00 UTC") + .where('date < $date', date: "#{year + 1}-01-01 00:00:00 UTC") + .as(:payments), 'payments.supporter_id=supporters.id') + .where('payments.sum > 25000') + .as('supporters') Psql.execute_vectors( Qexpr.new .select( - "supporters.*", + 'supporters.*', '(payments.gross_amount / 100.0)::money::text AS "Donation Amount"', 'payments.date AS "Donation Date"', 'payments.towards AS "Designation"' @@ -538,86 +527,84 @@ UNION DISTINCT .join(supporter_expr, 'supporters.id = payments.supporter_id') .where('payments.nonprofit_id = $id', id: np_id) .where('payments.date >= $date', date: "#{year}-01-01 00:00:00 UTC") - .where('payments.date < $date', date: "#{year+1}-01-01 00:00:00 UTC") - .order_by("supporters.\"MAX Name\", payments.date DESC") + .where('payments.date < $date', date: "#{year + 1}-01-01 00:00:00 UTC") + .order_by('supporters."MAX Name", payments.date DESC') ) end - # returns an array of common selects for supporters # which gets concated with an optional array of additional selects # used for merging supporters, crm profile and info card def self.profile_selects(arr = []) - ["supporters.id", - "supporters.name", - "supporters.email", - "supporters.address", - "supporters.state_code", - "supporters.city", - "supporters.zip_code", - "supporters.country", - "supporters.organization", - "supporters.phone"] + arr + ['supporters.id', + 'supporters.name', + 'supporters.email', + 'supporters.address', + 'supporters.state_code', + 'supporters.city', + 'supporters.zip_code', + 'supporters.country', + 'supporters.organization', + 'supporters.phone'] + arr end - # used on crm profile and info card - def self.profile_payments_subquery - Qx.select("supporter_id", "SUM(gross_amount)", "COUNT(id) AS count") - .from("payments") - .group_by("supporter_id") - .as("payments") + def self.profile_payments_subquery + Qx.select('supporter_id', 'SUM(gross_amount)', 'COUNT(id) AS count') + .from('payments') + .group_by('supporter_id') + .as('payments') end - # Get a large set of detailed info for a single supporter, to be displayed in # the side panel details of the supporter listing after clicking a row. def self.for_crm_profile(npo_id, ids) selects = [ - "supporters.created_at", - "supporters.imported_at", - "supporters.anonymous AS anon", - "supporters.is_unsubscribed_from_emails", - "COALESCE(MAX(payments.sum), 0) AS raised", - "COALESCE(MAX(payments.count), 0) AS payments_count", - "COALESCE(COUNT(recurring_donations.active), 0) AS recurring_donations_count", - "MAX(full_contact_infos.full_name) AS fc_full_name", - "MAX(full_contact_infos.age) AS fc_age", - "MAX(full_contact_infos.location_general) AS fc_location_general", - "MAX(full_contact_infos.websites) AS fc_websites"] + 'supporters.created_at', + 'supporters.imported_at', + 'supporters.anonymous AS anon', + 'supporters.is_unsubscribed_from_emails', + 'COALESCE(MAX(payments.sum), 0) AS raised', + 'COALESCE(MAX(payments.count), 0) AS payments_count', + 'COALESCE(COUNT(recurring_donations.active), 0) AS recurring_donations_count', + 'MAX(full_contact_infos.full_name) AS fc_full_name', + 'MAX(full_contact_infos.age) AS fc_age', + 'MAX(full_contact_infos.location_general) AS fc_location_general', + 'MAX(full_contact_infos.websites) AS fc_websites' + ] Qx.select(*QuerySupporters.profile_selects(selects)) - .from("supporters") + .from('supporters') .left_join( - ["donations", "donations.supporter_id=supporters.id"], - ["full_contact_infos", "full_contact_infos.supporter_id=supporters.id"], - ["recurring_donations", "recurring_donations.donation_id=donations.id"], - [QuerySupporters.profile_payments_subquery, "payments.supporter_id=supporters.id"]) - .group_by("supporters.id") - .where("supporters.id IN ($ids)", ids: ids) - .and_where("supporters.nonprofit_id = $id", id: npo_id) + ['donations', 'donations.supporter_id=supporters.id'], + ['full_contact_infos', 'full_contact_infos.supporter_id=supporters.id'], + ['recurring_donations', 'recurring_donations.donation_id=donations.id'], + [QuerySupporters.profile_payments_subquery, 'payments.supporter_id=supporters.id'] + ) + .group_by('supporters.id') + .where('supporters.id IN ($ids)', ids: ids) + .and_where('supporters.nonprofit_id = $id', id: npo_id) .execute end def self.for_info_card(id) - selects = ["COALESCE(MAX(payments.sum), 0) AS raised"] + selects = ['COALESCE(MAX(payments.sum), 0) AS raised'] Qx.select(*QuerySupporters.profile_selects(selects)) - .from("supporters") - .left_join([QuerySupporters.profile_payments_subquery, "payments.supporter_id=supporters.id"]) - .group_by("supporters.id") - .where("supporters.id=$id", id: id) + .from('supporters') + .left_join([QuerySupporters.profile_payments_subquery, 'payments.supporter_id=supporters.id']) + .group_by('supporters.id') + .where('supporters.id=$id', id: id) .execute.first end def self.merge_data(ids) Qx.select(*QuerySupporters.profile_selects) - .from("supporters") - .group_by("supporters.id") - .where("supporters.id IN ($ids)", ids: ids.split(',')) + .from('supporters') + .group_by('supporters.id') + .where('supporters.id IN ($ids)', ids: ids.split(',')) .execute end - def self.year_aggregate_report(npo_id, time_range_params) npo_id = npo_id.to_i @@ -626,10 +613,9 @@ UNION DISTINCT rescue ArgumentError => e raise ParamValidation::ValidationError.new(e.message, {}) end - ParamValidation.new({npo_id: npo_id}, { - npo_id: {required: true, is_integer: true} - }) - aggregate_dons = %Q( + ParamValidation.new({ npo_id: npo_id }, + npo_id: { required: true, is_integer: true }) + aggregate_dons = %( array_to_string( array_agg( payments.date::date || ' ' || @@ -642,51 +628,46 @@ UNION DISTINCT ) AS "Payment History" ) selects = supporter_export_selections.concat([ - "SUM(payments.gross_amount / 100)::text::money AS \"Total Payments\"", - "MAX(payments.date)::date AS \"Last Payment Date\"", - "AVG(payments.gross_amount / 100)::text::money AS \"Average Payment\"", - aggregate_dons - ]) - return Qx.select(selects) + 'SUM(payments.gross_amount / 100)::text::money AS "Total Payments"', + 'MAX(payments.date)::date AS "Last Payment Date"', + 'AVG(payments.gross_amount / 100)::text::money AS "Average Payment"', + aggregate_dons + ]) + Qx.select(selects) .from(:supporters) - .join("payments", "payments.supporter_id=supporters.id AND payments.date::date >= $min_date AND payments.date::date < $max_date",:min_date => min_date.to_date, :max_date => max_date.to_date ) + .join('payments', 'payments.supporter_id=supporters.id AND payments.date::date >= $min_date AND payments.date::date < $max_date', min_date: min_date.to_date, max_date: max_date.to_date) .where('supporters.nonprofit_id=$id', id: npo_id) - .group_by("supporters.id") + .group_by('supporters.id') .order_by("substring(trim(supporters.name) from '^.+ ([^\s]+)$')") .execute(format: 'csv') end - def self.get_min_or_max_dates_for_range(time_range_params) - begin - if (time_range_params[:year]) - if (time_range_params[:year].is_a?(Integer)) - return DateTime.new(time_range_params[:year], 1, 1), DateTime.new(time_range_params[:year]+1, 1, 1) - end - if (time_range_params[:year].is_a?(String)) - wip = time_range_params[:year].to_i - return DateTime.new(wip, 1, 1), DateTime.new(wip+1, 1, 1) - end + if time_range_params[:year] + if time_range_params[:year].is_a?(Integer) + return DateTime.new(time_range_params[:year], 1, 1), DateTime.new(time_range_params[:year] + 1, 1, 1) end - if (time_range_params[:start]) - start = parse_convert_datetime(time_range_params[:start]) - if (time_range_params[:end]) - end_datetime = parse_convert_datetime(time_range_params[:end]) - end - unless start.nil? - return start, end_datetime ? end_datetime : start + 1.year - end + if time_range_params[:year].is_a?(String) + wip = time_range_params[:year].to_i + return DateTime.new(wip, 1, 1), DateTime.new(wip + 1, 1, 1) end - raise ArgumentError.new("no valid time range provided") - rescue - raise ArgumentError.new("no valid time range provided") end + if time_range_params[:start] + start = parse_convert_datetime(time_range_params[:start]) + if time_range_params[:end] + end_datetime = parse_convert_datetime(time_range_params[:end]) + end + return start, end_datetime || start + 1.year unless start.nil? + end + raise ArgumentError, 'no valid time range provided' + rescue StandardError + raise ArgumentError, 'no valid time range provided' end def self.tag_joins(nonprofit_id, supporter_id) - Qx.select('tag_masters.id', 'tag_masters.name') + Qx.select('tag_masters.id', 'tag_masters.name') .from('tag_joins') .left_join('tag_masters', 'tag_masters.id = tag_joins.tag_master_id') .where( @@ -700,25 +681,18 @@ UNION DISTINCT # this is inefficient, don't use in live code def self.find_supporters_with_multiple_recurring_donations_evil_way(npo_id) supporters = Supporter.where('supporters.nonprofit_id = ?', npo_id).includes(:recurring_donations) - supporters.select{|s| s.recurring_donations.length > 1} + supporters.select { |s| s.recurring_donations.length > 1 } end # this is inefficient, don't use in live code def self.find_supporters_with_multiple_active_recurring_donations_evil_way(npo_id) supporters = Supporter.where('supporters.nonprofit_id = ?', npo_id).includes(:recurring_donations) - supporters.select{|s| s.recurring_donations.select{|rd| rd.active }.length > 1} + supporters.select { |s| s.recurring_donations.select(&:active).length > 1 } end def self.parse_convert_datetime(date) - if (date.is_a?(DateTime)) - return date - end - if (date.is_a?(Date)) - return date.to_datetime - end - if(date.is_a?(String)) - return DateTime.parse(date) - end + return date if date.is_a?(DateTime) + return date.to_datetime if date.is_a?(Date) + return DateTime.parse(date) if date.is_a?(String) end end - diff --git a/lib/query/query_ticket_levels.rb b/lib/query/query_ticket_levels.rb index 6e2fea0e..6e60d0a0 100644 --- a/lib/query/query_ticket_levels.rb +++ b/lib/query/query_ticket_levels.rb @@ -1,52 +1,52 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'hashie' module QueryTicketLevels - # Given an array of ticket hashes, where each hash has a ticket_level_id and a quantity, # calculate the gross amount for all the tickets # # This could probably be more efficient. I didn't think of a way to calculate it within the query itself. # Although I think it's O(n), and n will always be quite small (the number of tickets someone buys) def self.gross_amount_from_tickets(tickets, discount_id) - amounts = TicketLevel.where('id IN (?)', tickets.map{|h| h['ticket_level_id']}).map{|i| [i.id, i.amount]}.to_h - total = tickets.map{|t| amounts[t['ticket_level_id'].to_i].to_i * t['quantity'].to_i}.sum + amounts = TicketLevel.where('id IN (?)', tickets.map { |h| h['ticket_level_id'] }).map { |i| [i.id, i.amount] }.to_h + total = tickets.map { |t| amounts[t['ticket_level_id'].to_i].to_i * t['quantity'].to_i }.sum if discount_id perc = EventDiscount.find(discount_id).percent - total = total - (total * (perc / 100.0)).round + total -= (total * (perc / 100.0)).round end - return total + total end def self.with_event_id(event_id, is_admin) - expr = Qx.select("ticket_levels.*", "SUM(tickets.quantity) AS quantity") - .from(:ticket_levels) - .left_join([:tickets, "ticket_levels.id=tickets.ticket_level_id"]) - .group_by("ticket_levels.id") - .where("ticket_levels.event_id = $id", id: event_id) - .order_by("ticket_levels.order ASC, coalesce(ticket_levels.amount, 'Infinity'::float) ASC, LOWER(ticket_levels.name) ASC") # This puts free ticket levels at the bottom + expr = Qx.select('ticket_levels.*', 'SUM(tickets.quantity) AS quantity') + .from(:ticket_levels) + .left_join([:tickets, 'ticket_levels.id=tickets.ticket_level_id']) + .group_by('ticket_levels.id') + .where('ticket_levels.event_id = $id', id: event_id) + .order_by("ticket_levels.order ASC, coalesce(ticket_levels.amount, 'Infinity'::float) ASC, LOWER(ticket_levels.name) ASC") # This puts free ticket levels at the bottom - if !is_admin - expr = expr.and_where("coalesce(ticket_levels.admin_only, FALSE) = FALSE") + unless is_admin + expr = expr.and_where('coalesce(ticket_levels.admin_only, FALSE) = FALSE') end - return expr.execute + expr.execute end def self.verify_tickets_available(tickets) - tickets.each{|data| - if (data[:quantity] != 0) - tl = TicketLevel.find(data[:ticket_level_id]) - if tl.limit && tl.limit > 0 - already_sold = Ticket.where('ticket_level_id = ?', data[:ticket_level_id]).sum('tickets.quantity') - unless (already_sold + data[:quantity]) <= tl.limit - raise NotEnoughQuantityError.new(TicketLevel, data[:ticket_level_id], data[:quantity], "Oops! We sold out some of the tickets you wanted before ordering. Please refresh to see what tickets are still available.") - end - end - end - } - end + tickets.each do |data| + next unless data[:quantity] != 0 + tl = TicketLevel.find(data[:ticket_level_id]) + next unless tl.limit && tl.limit > 0 + + already_sold = Ticket.where('ticket_level_id = ?', data[:ticket_level_id]).sum('tickets.quantity') + unless (already_sold + data[:quantity]) <= tl.limit + raise NotEnoughQuantityError.new(TicketLevel, data[:ticket_level_id], data[:quantity], 'Oops! We sold out some of the tickets you wanted before ordering. Please refresh to see what tickets are still available.') + end + end + end end diff --git a/lib/query/query_tickets.rb b/lib/query/query_tickets.rb index f8582893..fc7fb696 100644 --- a/lib/query/query_tickets.rb +++ b/lib/query/query_tickets.rb @@ -1,72 +1,72 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueryTickets - def self.attendees_expr(event_id, query) expr = Qexpr.new - .from('tickets') - .where("coalesce(tickets.deleted, FALSE) = FALSE") - .left_outer_join("event_discounts", "event_discounts.id=tickets.event_discount_id") - .left_outer_join( - Qexpr.new.select("*") - .from(:supporters).group_by("id").as("supporters"), - 'tickets.supporter_id=supporters.id' - ) - .left_outer_join("charges", "charges.id=tickets.charge_id") - .left_outer_join( - Qexpr.new.select("charge_id", "SUM(coalesce(amount, 0)) AS amount") - .from(:refunds) - .group_by(:charge_id) - .as(:refunds), - "refunds.charge_id=charges.id" - ) - .left_outer_join( - Qexpr.new.select("id", "name", "amount") - .from(:ticket_levels).group_by("id").as("ticket_levels"), - 'tickets.ticket_level_id=ticket_levels.id' - ) - .left_outer_join( - Qexpr.new.select('token', 'tokenizable_id').from(:source_tokens).group_by( 'token', 'tokenizable_id').as('source_tokens'), - 'tickets.source_token_id=source_tokens.token' - ) - .left_outer_join( - # TODO this does not support anything other than cards! - Qexpr.new.select('id', 'name').from(:cards).group_by('id', 'name').as('cards'), - 'source_tokens.tokenizable_id = cards.id' - ) - .left_outer_join( - Qexpr.new.select('supporter_id', 'MAX(event_id) AS event_id', 'SUM(amount) AS total_amount') - .from(:donations).where("event_id=$id", id: event_id).group_by("supporter_id").as(:donations), - "donations.supporter_id=supporters.id AND donations.event_id=$id", id: event_id - ) - .where('tickets.event_id=$id', id: event_id) - .order_by('tickets.bid_id DESC') + .from('tickets') + .where('coalesce(tickets.deleted, FALSE) = FALSE') + .left_outer_join('event_discounts', 'event_discounts.id=tickets.event_discount_id') + .left_outer_join( + Qexpr.new.select('*') + .from(:supporters).group_by('id').as('supporters'), + 'tickets.supporter_id=supporters.id' + ) + .left_outer_join('charges', 'charges.id=tickets.charge_id') + .left_outer_join( + Qexpr.new.select('charge_id', 'SUM(coalesce(amount, 0)) AS amount') + .from(:refunds) + .group_by(:charge_id) + .as(:refunds), + 'refunds.charge_id=charges.id' + ) + .left_outer_join( + Qexpr.new.select('id', 'name', 'amount') + .from(:ticket_levels).group_by('id').as('ticket_levels'), + 'tickets.ticket_level_id=ticket_levels.id' + ) + .left_outer_join( + Qexpr.new.select('token', 'tokenizable_id').from(:source_tokens).group_by('token', 'tokenizable_id').as('source_tokens'), + 'tickets.source_token_id=source_tokens.token' + ) + .left_outer_join( + # TODO: this does not support anything other than cards! + Qexpr.new.select('id', 'name').from(:cards).group_by('id', 'name').as('cards'), + 'source_tokens.tokenizable_id = cards.id' + ) + .left_outer_join( + Qexpr.new.select('supporter_id', 'MAX(event_id) AS event_id', 'SUM(amount) AS total_amount') + .from(:donations).where('event_id=$id', id: event_id).group_by('supporter_id').as(:donations), + 'donations.supporter_id=supporters.id AND donations.event_id=$id', id: event_id + ) + .where('tickets.event_id=$id', id: event_id) + .order_by('tickets.bid_id DESC') if query[:search].present? query[:search] = "%#{query[:search].downcase.split(' ').join('%')}%" - expr = expr.where(%Q( + expr = expr.where(%( lower(supporters.name) LIKE $search OR lower(supporters.email) LIKE $search OR lower(ticket_levels.name) LIKE $search ), search: '%' + query[:search] + '%') end - if ['asc', 'desc'].include? query[:sort_attendee] + if %w[asc desc].include? query[:sort_attendee] expr = expr.order_by("lower(supporters.name) #{query[:sort_attendee]} NULLS LAST") end - if ['asc', 'desc'].include? query[:sort_id] + if %w[asc desc].include? query[:sort_id] expr = expr.order_by("tickets.bid_id #{query[:sort_id]}") end - if ['asc', 'desc'].include? query[:sort_note] + if %w[asc desc].include? query[:sort_note] expr = expr.order_by("lower(tickets.note) #{query[:sort_note]} NULLS LAST") end - if ['asc', 'desc'].include? query[:sort_ticket_level] + if %w[asc desc].include? query[:sort_ticket_level] expr = expr.order_by("lower(ticket_levels.name) #{query[:sort_ticket_level]} NULLS LAST") end - if ['asc', 'desc'].include? query[:sort_donation] + if %w[asc desc].include? query[:sort_donation] expr = expr.order_by("total_donations #{query[:sort_donation]} NULLS LAST") end - return expr + expr end - def self.attendees_list(event_id, query) limit = 30 offset = Qexpr.page_offset(limit, query[:page]) @@ -78,44 +78,42 @@ module QueryTickets ) total_count = Psql.execute( - Qexpr.new.select("COUNT(ts)") + Qexpr.new.select('COUNT(ts)') .from(attendees_expr(event_id, query) .remove(:order_by).select('tickets.id'), 'ts') ).first['count'] - #TODO this worries me. Seems like a recipe for slow returns... perhaps some caching of the tokens every so often? - data.each{|i| - unless (i['source_token_id'] && QuerySourceToken.source_token_unexpired?(SourceToken.find(i['source_token_id']))) + # TODO: this worries me. Seems like a recipe for slow returns... perhaps some caching of the tokens every so often? + data.each do |i| + unless i['source_token_id'] && QuerySourceToken.source_token_unexpired?(SourceToken.find(i['source_token_id'])) i['source_token_id'] = nil end - } + end - return { + { data: data, total_count: total_count, remaining: Qexpr.remaining_count(total_count, limit, query[:page]) } end - def self.for_export(event_id, query) Psql.execute_vectors( attendees_expr(event_id, query) .select([ - "tickets.bid_id AS id", - "ticket_levels.name AS ticket_level", - "MONEY((coalesce(charges.amount, 0) - coalesce(refunds.amount, 0)) / 100.0) AS ticket_cost", - "MONEY(coalesce(donations.total_amount, 0) / 100.0) AS total_donations", - "tickets.quantity", - "tickets.checked_in AS \"Checked In?\"", - "tickets.note", + 'tickets.bid_id AS id', + 'ticket_levels.name AS ticket_level', + 'MONEY((coalesce(charges.amount, 0) - coalesce(refunds.amount, 0)) / 100.0) AS ticket_cost', + 'MONEY(coalesce(donations.total_amount, 0) / 100.0) AS total_donations', + 'tickets.quantity', + 'tickets.checked_in AS "Checked In?"', + 'tickets.note', "CASE WHEN event_discounts.id IS NULL THEN 'None' ELSE concat(event_discounts.name, ' (', event_discounts.percent, '%)') END AS \"Discount\"", "CASE WHEN tickets.card_id IS NULL OR tickets.card_id = 0 THEN '' ELSE 'YES' END AS \"Card Saved?\"" ].concat(QuerySupporters.supporter_export_selections)) ) end - def self.attendees_list_selection ['tickets.id', 'tickets.bid_id', @@ -133,27 +131,25 @@ module QueryTickets 'supporters.email AS email', 'coalesce(donations.total_amount, 0) AS total_donations', 'source_tokens.token AS token', - 'cards.name AS card_name' - ] + 'cards.name AS card_name'] end def self.for_event_activities(event_id) selects = [" - CASE - WHEN supporters.anonymous='t' - OR supporters.name='' - OR supporters.name IS NULL - THEN 'A supporter' - ELSE supporters.name - END AS supporter_name", - 'tickets.quantity', 'tickets.created_at'] - return Qx.select(selects.join(',')) + CASE + WHEN supporters.anonymous='t' + OR supporters.name='' + OR supporters.name IS NULL + THEN 'A supporter' + ELSE supporters.name + END AS supporter_name", + 'tickets.quantity', 'tickets.created_at'] + Qx.select(selects.join(',')) .from(:tickets) .left_join(:supporters, 'tickets.supporter_id=supporters.id') .where('tickets.event_id=$id', id: event_id) - .order_by("tickets.created_at desc") + .order_by('tickets.created_at desc') .limit(15) .execute end - end diff --git a/lib/query/query_users.rb b/lib/query/query_users.rb index 94bcb3c2..579a9e72 100644 --- a/lib/query/query_users.rb +++ b/lib/query/query_users.rb @@ -1,40 +1,41 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'psql' require 'qexpr' require 'query/query_email_settings' module QueryUsers - # Return all the nonprofit user emails for a given email notification setting # for notification_type in ['payments', 'campaigns', 'events', 'payouts', 'recurring_donations'] def self.nonprofit_user_emails(np_id, notification_type) - raise ArgumentError.new('Invalid notification type') unless QueryEmailSettings::Settings.include?(notification_type) - Qx.select("users.email") - .from("users") - .join("roles", "roles.user_id=users.id") - .add_join("nonprofits", "roles.host_id=nonprofits.id AND roles.host_type='Nonprofit'") - .add_left_join("email_settings", "email_settings.user_id=users.id") + raise ArgumentError, 'Invalid notification type' unless QueryEmailSettings::Settings.include?(notification_type) + + Qx.select('users.email') + .from('users') + .join('roles', 'roles.user_id=users.id') + .add_join('nonprofits', "roles.host_id=nonprofits.id AND roles.host_type='Nonprofit'") + .add_left_join('email_settings', 'email_settings.user_id=users.id') .where("email_settings.user_id IS NULL OR email_settings.#{notification_type}=TRUE") - .and_where("nonprofits.id=$id", id: np_id) - .group_by("users.email") - .execute.map{|h| h['email']} + .and_where('nonprofits.id=$id', id: np_id) + .group_by('users.email') + .execute.map { |h| h['email'] } end # Return all nonprofit emails regardless of email settings - def self.all_nonprofit_user_emails(np_id, roles=[:nonprofit_admin, :nonprofit_user]) + def self.all_nonprofit_user_emails(np_id, roles = %i[nonprofit_admin nonprofit_user]) Qx.select('users.email').from('users') .join('roles', 'roles.user_id = users.id') .add_join('nonprofits', 'nonprofits.id = roles.host_id AND roles.host_type=\'Nonprofit\'') .where('nonprofits.id=$id', id: np_id) .and_where('roles.name IN ($names)', names: roles) - .execute.map{|h| h['email']} + .execute.map { |h| h['email'] } end # Return an array of email address strings for all users with role of 'super_admin' def self.super_admin_emails - Qx.select("users.email").from("users") - .join("roles", "roles.user_id=users.id AND roles.name='super_admin'") - .ex.map{|h| h['email']} + Qx.select('users.email').from('users') + .join('roles', "roles.user_id=users.id AND roles.name='super_admin'") + .ex.map { |h| h['email'] } end - end diff --git a/lib/queue_donations.rb b/lib/queue_donations.rb index d9eeea03..ccbbd8bf 100644 --- a/lib/queue_donations.rb +++ b/lib/queue_donations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module QueueDonations def self.execute_for_donation(id) @@ -10,15 +12,17 @@ module QueueDonations def self.execute_all donations = fetch_donations return if donations.empty? + donations_ids = donations.collect(&:id) execute(donations) end def self.dry_execute_all -puts "dry push donations to civi" + puts 'dry push donations to civi' donations = fetch_donations return if donations.empty? + donations_ids = donations.collect(&:id) dry_execute(donations) @@ -31,18 +35,16 @@ puts "dry push donations to civi" donations_ids = donations.collect(&:id) set_queued_for_import_at(donations_ids) - rescue Bunny::Exception, Bunny::ClientTimeout, Bunny::ConnectionTimeout Rails.logger.warn "Bunny error: QueueDonations.execute failed for ids #{donations_ids}" - return + nil end def self.dry_execute(donations) push(donations) - rescue Bunny::Exception, Bunny::ClientTimeout, Bunny::ConnectionTimeout Rails.logger.warn "Bunny error: QueueDonations.dry_execute failed for ids #{donations_ids}" - return + nil end def self.push(donations) @@ -68,16 +70,16 @@ puts "dry push donations to civi" def self.set_queued_for_import_at(ids) timestamp = Time.current - Qx.update(:donations). - where('id IN ($ids)', ids: ids). - set(queued_for_import_at: timestamp). - execute + Qx.update(:donations) + .where('id IN ($ids)', ids: ids) + .set(queued_for_import_at: timestamp) + .execute end def self.fetch_donations - Donation. - where('queued_for_import_at IS null'). - includes(:supporter, :nonprofit, :tracking, :payment,:recurring_donation) + Donation + .where('queued_for_import_at IS null') + .includes(:supporter, :nonprofit, :tracking, :payment, :recurring_donation) end def self.prepare_donation_params(donation) @@ -87,7 +89,7 @@ puts "dry push donations to civi" recurring = donation.recurring_donation action_type = :donate - action_technical_type = "cc.wemove.eu:donate" + action_technical_type = 'cc.wemove.eu:donate' action_name = "undefined_#{donation.supporter.locale}" external_id = campaign ? campaign.external_identifier : "cc_default_#{nonprofit.id}" @@ -118,7 +120,8 @@ puts "dry push donations to civi" { email: supporter.email } ], addresses: [ - { zip: supporter.zip_code, country: supporter.country }], + { zip: supporter.zip_code, country: supporter.country } + ] } end @@ -128,26 +131,26 @@ puts "dry push donations to civi" currency: donation.nonprofit.currency, recurring_id: donation.recurring_donation ? "cc_#{donation.recurring_donation.id}" : nil, external_identifier: "cc_#{donation.id}", - type: donation.recurring ? "recurring" : "single" + type: donation.recurring ? 'recurring' : 'single' } if donation.card_id - data = common_data.merge({ - payment_processor: "stripe", + data = common_data.merge( + payment_processor: 'stripe', amount_charged: donation.payment.charge.amount / 100.0, transaction_id: donation.payment.charge.stripe_charge_id, - status: donation.payment.charge.paid? ? "success" : "not_paid" - }) + status: donation.payment.charge.paid? ? 'success' : 'not_paid' + ) elsif donation.direct_debit_detail_id - data = common_data.merge({ - payment_processor: "sepa", + data = common_data.merge( + payment_processor: 'sepa', amount_charged: 0, transaction_id: "cc_#{donation.id}", iban: donation.direct_debit_detail.iban, bic: donation.direct_debit_detail.bic, account_holder: donation.direct_debit_detail.account_holder_name, - status: "success" - }) + status: 'success' + ) end data @@ -158,9 +161,10 @@ puts "dry push donations to civi" id: recurring.id, start: recurring.start_date, time_unit: recurring.time_unit, - active: recurring.active, + active: recurring.active } end + def self.tracking_data(tracking) { source: tracking.utm_source, diff --git a/lib/qx_query_chunker.rb b/lib/qx_query_chunker.rb index cee3ca0b..f2303a06 100644 --- a/lib/qx_query_chunker.rb +++ b/lib/qx_query_chunker.rb @@ -1 +1,3 @@ -QxQueryChunker = QexprQueryChunker \ No newline at end of file +# frozen_string_literal: true + +QxQueryChunker = QexprQueryChunker diff --git a/lib/required_keys.rb b/lib/required_keys.rb index c4d75e53..64b9e0f4 100644 --- a/lib/required_keys.rb +++ b/lib/required_keys.rb @@ -1,12 +1,13 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Given a hash and an array of keys # Raise an argument error if any keys are missing from the hash class RequiredKeys - def initialize(hash, keys) - missing = keys.select{|k| hash[k].nil?} - raise ArgumentError.new("Missing keys: #{missing}") if missing.any? + missing = keys.select { |k| hash[k].nil? } + raise ArgumentError, "Missing keys: #{missing}" if missing.any? end end diff --git a/lib/retrieve/retrieve_active_record_items.rb b/lib/retrieve/retrieve_active_record_items.rb index a0b172a9..3a79461f 100644 --- a/lib/retrieve/retrieve_active_record_items.rb +++ b/lib/retrieve/retrieve_active_record_items.rb @@ -1,45 +1,49 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module RetrieveActiveRecordItems - def self.retrieve(data, optional= false) - data.map{|k,v| - our_integer = Integer(v) rescue nil + def self.retrieve(data, optional = false) + data.map do |k, v| + our_integer = begin + Integer(v) + rescue StandardError + nil + end unless (optional && v.nil?) || (our_integer && our_integer > 0) - raise ArgumentError.new("Value '#{v}' for Key '#{k}' is not valid") + raise ArgumentError, "Value '#{v}' for Key '#{k}' is not valid" end - unless k.is_a? Class - raise ArgumentError.new("Key '#{k.to_s}' is not a class") - end + raise ArgumentError, "Key '#{k}' is not a class" unless k.is_a? Class + ret = [] if optional && v.nil? ret = [k, nil] else ret = [k, k.where('id = ?', our_integer).first] - if (ret[1] == nil) - raise ParamValidation::ValidationError.new("ID #{v} is not a valid #{k.to_s}", {key: k}) + if ret[1].nil? + raise ParamValidation::ValidationError.new("ID #{v} is not a valid #{k}", key: k) end end ret - }.to_h + end.to_h end - def self.retrieve_from_keys(input, class_to_key_hash, optional=false) - class_to_key_hash.map{|k,v| - unless k.is_a? Class - raise ArgumentError.new("Key '#{k.to_s}' is not a class") - end + def self.retrieve_from_keys(input, class_to_key_hash, optional = false) + class_to_key_hash.map do |k, v| + raise ArgumentError, "Key '#{k}' is not a class" unless k.is_a? Class + ret = nil begin - item = retrieve({k => input[v]}, optional) + item = retrieve({ k => input[v] }, optional) ret = [v, item[k]] rescue ParamValidation::ValidationError - raise ParamValidation::ValidationError.new("ID #{input[v]} is not a valid #{k.to_s}", {key: v}) + raise ParamValidation::ValidationError.new("ID #{input[v]} is not a valid #{k}", key: v) rescue ArgumentError - raise ParamValidation::ValidationError.new("#{input[v]} is not a valid ID for Key '#{v}'", {key: v}) - rescue - raise ParamValidation::ValidationError.new("#{input[v]} is not a valid ID for Key '#{v}'", {key: v}) + raise ParamValidation::ValidationError.new("#{input[v]} is not a valid ID for Key '#{v}'", key: v) + rescue StandardError + raise ParamValidation::ValidationError.new("#{input[v]} is not a valid ID for Key '#{v}'", key: v) end ret - }.to_h + end.to_h end -end \ No newline at end of file +end diff --git a/lib/scheduled_jobs.rb b/lib/scheduled_jobs.rb index 3d4f992d..13ec5dbc 100644 --- a/lib/scheduled_jobs.rb +++ b/lib/scheduled_jobs.rb @@ -1,9 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' -require 'enumerator' - module ScheduledJobs - # Each of these functions should return an Enumerator # Each value in the enumerator should be a lambda # That way the heroku_scheduled_job task can iterate over each lambda @@ -15,41 +14,38 @@ module ScheduledJobs def self.delete_junk_data # Delete all custom fields with emptly/nil vals del_cfjs_noval = Qx.delete_from(:custom_field_joins) - .where("value IS NULL OR value=''") + .where("value IS NULL OR value=''") # Delete orphaned custom field joins (those should also all have supporters) - del_cfjs_orphaned = Qx.delete_from(:custom_field_joins).where("id IN ($ids)", { - ids: Qx.select("custom_field_joins.id") - .from(:custom_field_joins) - .left_join("supporters", "custom_field_joins.supporter_id=supporters.id") - .where("supporters.id IS NULL") - }) + del_cfjs_orphaned = Qx.delete_from(:custom_field_joins).where('id IN ($ids)', + ids: Qx.select('custom_field_joins.id') + .from(:custom_field_joins) + .left_join('supporters', 'custom_field_joins.supporter_id=supporters.id') + .where('supporters.id IS NULL')) # Delete orphaned tag joins - del_tags_orphaned = Qx.delete_from(:tag_joins).where("id IN ($ids)", { - ids: Qx.select("tag_joins.id") - .from(:tag_joins) - .left_join(:supporters, "tag_joins.supporter_id=supporters.id") - .where("supporters.id IS NULL") - }) + del_tags_orphaned = Qx.delete_from(:tag_joins).where('id IN ($ids)', + ids: Qx.select('tag_joins.id') + .from(:tag_joins) + .left_join(:supporters, 'tag_joins.supporter_id=supporters.id') + .where('supporters.id IS NULL')) - return Enumerator.new do |yielder| + Enumerator.new do |yielder| yielder << lambda do del_cfjs_noval.execute - "Successfully cleaned up custom field joins with no values" + 'Successfully cleaned up custom field joins with no values' end yielder << lambda do del_cfjs_orphaned.execute - "Successfully cleaned up custom field joins that have been orphaned from supporters" + 'Successfully cleaned up custom field joins that have been orphaned from supporters' end yielder << lambda do del_tags_orphaned.execute - "Successfully cleaned up tags that have been orphaned from supporters" + 'Successfully cleaned up tags that have been orphaned from supporters' end end end - def self.pay_recurring_donations - return Enumerator.new do |yielder| + Enumerator.new do |yielder| yielder << lambda do ids = PayRecurringDonation.pay_all_due_with_stripe "Queued jobs to pay #{ids.count} total recurring donations\n Recurring Donation Ids to run are: \n#{ids.join('\n')}" @@ -58,7 +54,7 @@ module ScheduledJobs end def self.update_verification_statuses - return Enumerator.new do |yielder| + Enumerator.new do |yielder| Nonprofit.where(verification_status: 'pending').each do |np| yielder << lambda do acct = Stripe::Account.retrieve(np.stripe_account_id) @@ -74,8 +70,8 @@ module ScheduledJobs end def self.update_np_balances - return Enumerator.new do |yielder| - nps = Nonprofit.where("id IN (?)", Charge.pending.uniq.pluck(:nonprofit_id)) + Enumerator.new do |yielder| + nps = Nonprofit.where('id IN (?)', Charge.pending.uniq.pluck(:nonprofit_id)) nps.each do |np| yielder << lambda do UpdateNonprofit.mark_available_charges(np.id) @@ -86,13 +82,12 @@ module ScheduledJobs end def self.update_pending_payouts - return Enumerator.new do |yielder| + Enumerator.new do |yielder| Payout.pending.includes(:nonprofit).each do |p| yielder << lambda do err = false - p.status = Stripe::Transfer.retrieve(p.stripe_transfer_id, { - stripe_account: p.nonprofit.stripe_account_id - }).status + p.status = Stripe::Transfer.retrieve(p.stripe_transfer_id, + stripe_account: p.nonprofit.stripe_account_id).status p.save "Updated status for NP #{p.nonprofit.id}, payout # #{p.id}" end @@ -101,9 +96,9 @@ module ScheduledJobs end def self.delete_expired_source_tokens - return Enumerator.new do |yielder| + Enumerator.new do |yielder| yielder << lambda do - tokens_deleted = SourceToken.where("expiration > ?", DateTime.now - 1.day).delete_all + tokens_deleted = SourceToken.where('expiration > ?', DateTime.now - 1.day).delete_all "Deleted #{tokens_deleted} source tokens" end end diff --git a/lib/search_vector.rb b/lib/search_vector.rb index 9b190382..dde98873 100644 --- a/lib/search_vector.rb +++ b/lib/search_vector.rb @@ -1,13 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module SearchVector + AcceptedTables = %w[supporters payments].freeze - AcceptedTables = ['supporters', 'payments'] - - def self.query(query_string, expr=nil) + def self.query(query_string, expr = nil) (expr || Qexpr.new).where( "to_tsvector('english', coalesce(supporters.name, '') || ' ' || coalesce(supporters.email, '')) @@ plainto_tsquery('english', $search)", - { search: query_string} + search: query_string ) end @@ -27,18 +28,18 @@ module SearchVector , donations.dedication ) AS search_blob" ) - .from(:payments) - .left_outer_join('supporters', 'payments.supporter_id=supporters.id') - .left_outer_join('donations', 'payments.donation_id=donations.id') + .from(:payments) + .left_outer_join('supporters', 'payments.supporter_id=supporters.id') + .left_outer_join('donations', 'payments.donation_id=donations.id') end # Construct of query of ids and search blobs for all supporters # for use in a sub-query def self._supporters_blob_query - fields_subquery = Qexpr.new.select("string_agg(value::text, ' ') AS value", "supporter_id") - .from(:custom_field_joins) - .group_by(:supporter_id) - .as(:custom_field_joins) + fields_subquery = Qexpr.new.select("string_agg(value::text, ' ') AS value", 'supporter_id') + .from(:custom_field_joins) + .group_by(:supporter_id) + .as(:custom_field_joins) Qexpr.new.select( 'supporters.id', "concat_ws(' ' @@ -55,10 +56,9 @@ module SearchVector , payments.towards ) AS search_blob" ) - .from(:supporters) - .left_outer_join(:payments, "payments.supporter_id=supporters.id") - .left_outer_join(:donations, "donations.supporter_id=supporters.id") - .left_outer_join(fields_subquery, "custom_field_joins.supporter_id=supporters.id") + .from(:supporters) + .left_outer_join(:payments, 'payments.supporter_id=supporters.id') + .left_outer_join(:donations, 'donations.supporter_id=supporters.id') + .left_outer_join(fields_subquery, 'custom_field_joins.supporter_id=supporters.id') end - end diff --git a/lib/slug_copy_naming_algorithm.rb b/lib/slug_copy_naming_algorithm.rb index c12bde98..0927265f 100644 --- a/lib/slug_copy_naming_algorithm.rb +++ b/lib/slug_copy_naming_algorithm.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SlugCopyNamingAlgorithm < CopyNamingAlgorithm - attr_accessor :klass, :nonprofit_id # @param [Class] klass def initialize(klass, nonprofit_id) - @klass = klass - @nonprofit_id = nonprofit_id + @klass = klass + @nonprofit_id = nonprofit_id end def copy_addition - "_copy" + '_copy' end def max_copies @@ -21,8 +22,7 @@ class SlugCopyNamingAlgorithm < CopyNamingAlgorithm end def get_already_used_name_entities(base_name) - end_name = "\\_copy\\_\\d{2}" + end_name = '\\_copy\\_\\d{2}' @klass.method(:where).call('slug SIMILAR TO ? AND nonprofit_id = ? AND (deleted IS NULL OR deleted = false)', base_name + end_name, nonprofit_id).select('slug') end - -end \ No newline at end of file +end diff --git a/lib/slug_nonprofit_naming_algorithm.rb b/lib/slug_nonprofit_naming_algorithm.rb index 010acc3c..7fe78721 100644 --- a/lib/slug_nonprofit_naming_algorithm.rb +++ b/lib/slug_nonprofit_naming_algorithm.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SlugNonprofitNamingAlgorithm < CopyNamingAlgorithm + attr_accessor :state_slug, :city_slug - attr_accessor :state_slug, :city_slug - - def initialize( state_slug, city_slug) - @state_slug = state_slug - @city_slug = city_slug + def initialize(state_slug, city_slug) + @state_slug = state_slug + @city_slug = city_slug end def copy_addition - "" + '' end def max_copies @@ -25,8 +26,7 @@ class SlugNonprofitNamingAlgorithm < CopyNamingAlgorithm end def get_already_used_name_entities(base_name) - end_name = "\\-\\d{2}" - Nonprofit.method(:where).call('slug SIMILAR TO ? AND state_code_slug = ? AND city_slug = ?', base_name + end_name, @state_slug, @city_slug).select('slug') + end_name = '\\-\\d{2}' + Nonprofit.method(:where).call('slug SIMILAR TO ? AND state_code_slug = ? AND city_slug = ?', base_name + end_name, @state_slug, @city_slug).select('slug') end - -end \ No newline at end of file +end diff --git a/lib/slug_p2p_campaign_naming_algorithm.rb b/lib/slug_p2p_campaign_naming_algorithm.rb index cbbe6aeb..c6bcdba2 100644 --- a/lib/slug_p2p_campaign_naming_algorithm.rb +++ b/lib/slug_p2p_campaign_naming_algorithm.rb @@ -1,14 +1,15 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SlugP2pCampaignNamingAlgorithm < CopyNamingAlgorithm - attr_accessor :nonprofit_id # @param [Integer] nonprofit_id def initialize(nonprofit_id) - @nonprofit_id = nonprofit_id + @nonprofit_id = nonprofit_id end def copy_addition - "" + '' end def max_copies @@ -20,8 +21,7 @@ class SlugP2pCampaignNamingAlgorithm < CopyNamingAlgorithm end def get_already_used_name_entities(base_name) - end_name = "\\_\\d{3}" + end_name = '\\_\\d{3}' Campaign.where('slug SIMILAR TO ? AND nonprofit_id = ?', base_name + end_name, nonprofit_id).select('slug') end - -end \ No newline at end of file +end diff --git a/lib/stripe_account.rb b/lib/stripe_account.rb index eaa236da..a4c111dd 100644 --- a/lib/stripe_account.rb +++ b/lib/stripe_account.rb @@ -1,17 +1,18 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module StripeAccount - # Returns the stripe account ID string def self.find_or_create(nonprofit_id) - ParamValidation.new({:nonprofit_id => nonprofit_id}, {:nonprofit_id => {:required=> true, :is_integer => true}}) - begin - np = Nonprofit.find(nonprofit_id) - rescue => e - raise ParamValidation::ValidationError.new("#{nonprofit_id} is not a valid nonprofit", {:key => :nonprofit_id}) - end + ParamValidation.new({ nonprofit_id: nonprofit_id }, nonprofit_id: { required: true, is_integer: true }) + begin + np = Nonprofit.find(nonprofit_id) + rescue StandardError => e + raise ParamValidation::ValidationError.new("#{nonprofit_id} is not a valid nonprofit", key: :nonprofit_id) + end - if !np['stripe_account_id'] + if !np['stripe_account_id'] return create(np) else return np['stripe_account_id'] @@ -20,32 +21,32 @@ module StripeAccount # np should be a hash with string keys def self.create(np) - ParamValidation.new({:np => np}, {:np => {:required=> true, :is_a => Nonprofit}}) - params = { - managed: true, - email: np['email'].present? ? np['email'] : np.roles.nonprofit_admins.order('created_at ASC').first.user.email, - business_name: np['name'], - legal_entity: { - type: 'company', - address: { - city: np['city'], - state: np['state_code'], - postal_code: np['zip_code'], - country: 'US' - }, - business_name: np['name'], - }, - product_description: 'Nonprofit donations', - transfer_schedule: { interval: 'manual' } - } + ParamValidation.new({ np: np }, np: { required: true, is_a: Nonprofit }) + params = { + managed: true, + email: np['email'].present? ? np['email'] : np.roles.nonprofit_admins.order('created_at ASC').first.user.email, + business_name: np['name'], + legal_entity: { + type: 'company', + address: { + city: np['city'], + state: np['state_code'], + postal_code: np['zip_code'], + country: 'US' + }, + business_name: np['name'] + }, + product_description: 'Nonprofit donations', + transfer_schedule: { interval: 'manual' } + } - if np['website'] && np['website'] =~ URI::regexp - params[:business_url] = np['website'] - end + if np['website'] && np['website'] =~ URI::DEFAULT_PARSER.make_regexp + params[:business_url] = np['website'] + end - acct = Stripe::Account.create(params) + acct = Stripe::Account.create(params) Qx.update(:nonprofits).set(stripe_account_id: acct.id).where(id: np['id']).execute NonprofitMailer.delay.setup_verification(np['id']) - return acct.id - end + acct.id + end end diff --git a/lib/stripe_utils.rb b/lib/stripe_utils.rb index 820a15ea..7a32c443 100644 --- a/lib/stripe_utils.rb +++ b/lib/stripe_utils.rb @@ -1,35 +1,35 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'stripe' require 'calculate/calculate_fees' module StripeUtils + # Get the verification status from a stripe object + # Some of our accounts seem to be marked 'Unverified,' but have no + # fields_needed set and have transfers_enabled set to true. So for our system, + # that practically means they are verified. + def self.get_verification_status(stripe_acct) + return 'verified' if stripe_acct.transfers_enabled - # Get the verification status from a stripe object - # Some of our accounts seem to be marked 'Unverified,' but have no - # fields_needed set and have transfers_enabled set to true. So for our system, - # that practically means they are verified. - def self.get_verification_status(stripe_acct) - return 'verified' if stripe_acct.transfers_enabled - return stripe_acct.legal_entity.verification.status - end + stripe_acct.legal_entity.verification.status + end - def self.create_transfer(net_amount, stripe_account_id, currency) - Stripe::Transfer.create({ - amount: net_amount, - currency: currency || Settings.intntl.currencies[0], - recipient: 'self' - }, { - stripe_account: stripe_account_id - }) - end + def self.create_transfer(net_amount, stripe_account_id, currency) + Stripe::Transfer.create({ + amount: net_amount, + currency: currency || Settings.intntl.currencies[0], + recipient: 'self' + }, + stripe_account: stripe_account_id) + end - - def self.create_refund(stripe_charge, amount, reason) - stripe_charge.refunds.create({ - amount: amount, - refund_application_fee: true, - reverse_transfer: true, - reason: reason - }) - end + def self.create_refund(stripe_charge, amount, reason) + stripe_charge.refunds.create( + amount: amount, + refund_application_fee: true, + reverse_transfer: true, + reason: reason + ) + end end diff --git a/lib/tasks/civicrm.rake b/lib/tasks/civicrm.rake index 4b040383..dae3cb1e 100644 --- a/lib/tasks/civicrm.rake +++ b/lib/tasks/civicrm.rake @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'queue_donations' namespace :civicrm do - desc "pushes donation records to CiviCRM" - task :push => :environment do + desc 'pushes donation records to CiviCRM' + task push: :environment do QueueDonations.execute_all end desc "pushes donation records to CiviCRM, but doesn't mark them as pushed (useful for debugging)" - task :dry_run => :environment do + task dry_run: :environment do QueueDonations.dry_execute_all end end diff --git a/lib/tasks/database.rake b/lib/tasks/database.rake index ca0636b2..3f6a4a3b 100644 --- a/lib/tasks/database.rake +++ b/lib/tasks/database.rake @@ -1,24 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Rake::Task["db:structure:dump"].clear +Rake::Task['db:structure:dump'].clear namespace :db do namespace :structure do - desc "Overriding the task db:structure:dump task to remove -i option from pg_dump to make postgres 9.5 compatible" - task dump: [:environment, :load_config] do + desc 'Overriding the task db:structure:dump task to remove -i option from pg_dump to make postgres 9.5 compatible' + task dump: %i[environment load_config] do config = ActiveRecord::Base.configurations[Rails.env] set_psql_env(config) - filename = File.join(Rails.root, "db", "structure.sql") - database = config["database"] + filename = File.join(Rails.root, 'db', 'structure.sql') + database = config['database'] command = "pg_dump -s -x -O -f #{Shellwords.escape(filename)} #{Shellwords.escape(database)}" raise 'Error dumping database' unless Kernel.system(command) - File.open(filename, "a") { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } + File.open(filename, 'a') { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } if ActiveRecord::Base.connection.supports_migrations? - File.open(filename, "a") do |f| + File.open(filename, 'a') do |f| f.puts ActiveRecord::Base.connection.dump_schema_information f.print "\n" end end - Rake::Task["db:structure:dump"].reenable + Rake::Task['db:structure:dump'].reenable end end @@ -28,4 +30,4 @@ namespace :db do ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] end -end \ No newline at end of file +end diff --git a/lib/tasks/full_contact.rake b/lib/tasks/full_contact.rake index 7ef13ced..8a6b983a 100644 --- a/lib/tasks/full_contact.rake +++ b/lib/tasks/full_contact.rake @@ -1,12 +1,13 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -desc "For generating Full Contact data" +desc 'For generating Full Contact data' # Clear old activerecord sessions tables daily -task :work_full_contact_queue => :environment do - +task work_full_contact_queue: :environment do loop do - sleep(10) until Qx.select("COUNT(*)").from("full_contact_jobs").execute.first['count'] > 0 - puts "working..." + sleep(10) until Qx.select('COUNT(*)').from('full_contact_jobs').execute.first['count'] > 0 + puts 'working...' begin InsertFullContactInfos.work_queue diff --git a/lib/tasks/health_report.rake b/lib/tasks/health_report.rake index daa1a910..e9f965af 100644 --- a/lib/tasks/health_report.rake +++ b/lib/tasks/health_report.rake @@ -1,10 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -desc "For sending an activity report email of what has been happening on the system" +desc 'For sending an activity report email of what has been happening on the system' # Clear old activerecord sessions tables daily -task :send_health_report => :environment do - GenericMailer.admin_notice({ +task send_health_report: :environment do + GenericMailer.admin_notice( body: HealthReport.format_data(HealthReport.query_data), subject: "CommitChange activity report #{Format::Date.to_readable(Time.now)}" - }).deliver + ).deliver end diff --git a/lib/tasks/oapi.rake b/lib/tasks/oapi.rake index 4bb6cf8d..46fb359a 100644 --- a/lib/tasks/oapi.rake +++ b/lib/tasks/oapi.rake @@ -1,8 +1,10 @@ +# frozen_string_literal: true + require 'grape-swagger/rake/oapi_tasks' namespace :oapi do task gen: [:environment] do ENV['store'] = 'tmp/openapi.json' GrapeSwagger::Rake::OapiTasks.new(Houdini::API) - Rake::Task['oapi:fetch'].invoke() + Rake::Task['oapi:fetch'].invoke end -end \ No newline at end of file +end diff --git a/lib/tasks/scheduler.rake b/lib/tasks/scheduler.rake index 37bade48..6ba7837d 100644 --- a/lib/tasks/scheduler.rake +++ b/lib/tasks/scheduler.rake @@ -1,25 +1,25 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'scheduled_jobs' desc "For use with Heroku's Scheduler add-on" # We use a single rake call so we can catch and send any errors that happen in the job -task :heroku_scheduled_job, [:name] => :environment do |t, args| +task :heroku_scheduled_job, [:name] => :environment do |_t, args| job_name = args[:name] # Fetch all the super admin emails so we can send a report enum = ScheduledJobs.send(job_name) - results = "" + results = '' enum.each do |lamb| - begin - result = lamb.call - results += "Success: #{result}\n" - rescue Exception => e - results += "Failure: #{e}\n" - end + result = lamb.call + results += "Success: #{result}\n" + rescue Exception => e + results += "Failure: #{e}\n" end - GenericMailer.delay.admin_notice({ + GenericMailer.delay.admin_notice( subject: "Scheduled job results on CommitChange for '#{job_name}'", - body: results.empty? ? "No jobs to run today." : results - }) + body: results.empty? ? 'No jobs to run today.' : results + ) end diff --git a/lib/tasks/seed.rake b/lib/tasks/seed.rake index 8d72629a..4ec8db2d 100644 --- a/lib/tasks/seed.rake +++ b/lib/tasks/seed.rake @@ -1,16 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later namespace :seed do - - task :np => :environment do - ActiveRecord::Base.transaction do - supers = Role.super_admins.includes(:user).map{|r| r.user} - n = Nonprofit.register(supers.last, name: "Testify #{rand(0..100)}", city: 'Albuquerque', state_code: 'NM') - n.verification_status = 'verified' - n.vetted = true - n.create_billing_subscription({billing_plan: BillingPlan.where(tier: 2).last}) - n.save! - supers.each{|user| user.roles.create(name: :nonprofit_admin, host: n)} - puts "New test nonprofit id: #{n.id}" - end - end + task np: :environment do + ActiveRecord::Base.transaction do + supers = Role.super_admins.includes(:user).map(&:user) + n = Nonprofit.register(supers.last, name: "Testify #{rand(0..100)}", city: 'Albuquerque', state_code: 'NM') + n.verification_status = 'verified' + n.vetted = true + n.create_billing_subscription(billing_plan: BillingPlan.where(tier: 2).last) + n.save! + supers.each { |user| user.roles.create(name: :nonprofit_admin, host: n) } + puts "New test nonprofit id: #{n.id}" + end + end end diff --git a/lib/tasks/settings.rake b/lib/tasks/settings.rake index fcb1fbb0..3a344add 100644 --- a/lib/tasks/settings.rake +++ b/lib/tasks/settings.rake @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later namespace :settings do @@ -5,33 +7,30 @@ namespace :settings do require File.expand_path('../../config/environment.rb', File.dirname(__FILE__)) end - desc "show settings" - task :show => :environment do + desc 'show settings' + task show: :environment do require 'pp' pp Settings.to_hash end - task :generate_json => :environment do - - cdn_url= URI(Settings.cdn.url) + task generate_json: :environment do + cdn_url = URI(Settings.cdn.url) cdn_url = cdn_url.to_s - if (Settings.button_config&.url) - cdn_url= URI(Settings.button_config.url).to_s + if Settings.button_config&.url + cdn_url = URI(Settings.button_config.url).to_s end - c = {button:{url:cdn_url,css:"#{cdn_url}/css/donate-button.v2.css"}} + c = { button: { url: cdn_url, css: "#{cdn_url}/css/donate-button.v2.css" } } open(File.expand_path('config/settings.json', Rails.root), 'w') do |f| f.write(c.to_json) end end - task :combine_translations => 'i18n:js:export' do + task combine_translations: 'i18n:js:export' do js_root = File.expand_path('public/javascripts', Rails.root) - #i18n = File.read(File.join(js_root, 'i18n.js')) + # i18n = File.read(File.join(js_root, 'i18n.js')) translations = File.read(File.join(js_root, 'translations.js')) open(File.join(js_root, '_final.js'), 'w') do |f| f.write("const I18n = require('i18n-js');\n" + translations + "\n window.I18n = I18n") end end - end - diff --git a/lib/timespan.rb b/lib/timespan.rb index 305d030e..d9f05d9c 100644 --- a/lib/timespan.rb +++ b/lib/timespan.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # For tracking and calculating timespans/time intervals # Relies on activesupport Timespan = Struct.new(:interval, :time_unit) do - - Units = ['week', 'day', 'month', 'year'] + Units = %w[week day month year].freeze TimeUnits = { '1_week' => 1.week.ago, '2_weeks' => 2.weeks.ago, @@ -13,35 +14,35 @@ Timespan = Struct.new(:interval, :time_unit) do '6_months' => 6.months.ago, '1_year' => 1.year.ago, '2_years' => 2.years.ago - } + }.freeze - # Test if end_date is past start_date by timespan - # eg: later_than_by?(Jun 13th, Jul 14th, 1.month) -> true - # Special case: - # later_than_by?(Jan 31st, Feb 28th, 1.month) -> true - def self.later_than_by?(start_date, end_date, timespan) - return (start_date + timespan) <= end_date - end + # Test if end_date is past start_date by timespan + # eg: later_than_by?(Jun 13th, Jul 14th, 1.month) -> true + # Special case: + # later_than_by?(Jan 31st, Feb 28th, 1.month) -> true + def self.later_than_by?(start_date, end_date, timespan) + (start_date + timespan) <= end_date + end - # Given an Integer (frequency) and a String (time unit), - # return the timespan object (ie. number of seconds) constituting the timespan - # timespan(1, 'minute') -> 60 - # timespan(1, 'month') -> 2592000 - def self.create(interval, time_unit) - raise(ArgumentError, "time_unit must be one of: #{Units}") unless Units.include?(time_unit) - return interval.send(time_unit.to_sym) - end + # Given an Integer (frequency) and a String (time unit), + # return the timespan object (ie. number of seconds) constituting the timespan + # timespan(1, 'minute') -> 60 + # timespan(1, 'month') -> 2592000 + def self.create(interval, time_unit) + raise(ArgumentError, "time_unit must be one of: #{Units}") unless Units.include?(time_unit) - def self.in_future?(datetime) - datetime > Time.current - end + interval.send(time_unit.to_sym) + end - def self.date_now_or_in_future?(date) - date >= Date.today - end + def self.in_future?(datetime) + datetime > Time.current + end - def self.in_past?(date) - date < Time.current - end + def self.date_now_or_in_future?(date) + date >= Date.today + end + def self.in_past?(date) + date < Time.current + end end diff --git a/lib/update/update_activities.rb b/lib/update/update_activities.rb index 89a96eae..1652d4cf 100644 --- a/lib/update/update_activities.rb +++ b/lib/update/update_activities.rb @@ -1,22 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module UpdateActivities - def self.for_supporter_notes(note) - user_email = Qx.select('email') - .from(:users) - .where(id: note[:user_id]) - .execute - .first['email'] + .from(:users) + .where(id: note[:user_id]) + .execute + .first['email'] Qx.update(:activities) - .set(json_data: {content: note[:content], user_email: user_email}.to_json) + .set(json_data: { content: note[:content], user_email: user_email }.to_json) .timestamps .where(attachment_id: note[:id]) .execute - - end + end end - diff --git a/lib/update/update_billing_subscriptions.rb b/lib/update/update_billing_subscriptions.rb index 5e15ae16..2a73e813 100644 --- a/lib/update/update_billing_subscriptions.rb +++ b/lib/update/update_billing_subscriptions.rb @@ -1,7 +1,8 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateBillingSubscriptions - def self.activate_from_trial(np_id) Qx.update(:billing_subscriptions) .set(status: 'active') diff --git a/lib/update/update_campaign_gift_option.rb b/lib/update/update_campaign_gift_option.rb index ba52f088..5163f2dc 100644 --- a/lib/update/update_campaign_gift_option.rb +++ b/lib/update/update_campaign_gift_option.rb @@ -1,10 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateCampaignGiftOption - - def self.update gift_option, params - gift_option.update_attributes params - return gift_option - end - + def self.update(gift_option, params) + gift_option.update_attributes params + gift_option + end end - diff --git a/lib/update/update_charges.rb b/lib/update/update_charges.rb index 0e3c957b..14450b4f 100644 --- a/lib/update/update_charges.rb +++ b/lib/update/update_charges.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateCharges + def self.disburse_all_with_payments(payment_ids) + Psql.execute(Qexpr.new.update(:charges, status: 'disbursed').where('payment_id IN ($ids)', ids: payment_ids).returning('id', 'status')) + end - def self.disburse_all_with_payments(payment_ids) - Psql.execute(Qexpr.new.update(:charges, status: 'disbursed').where("payment_id IN ($ids)", ids: payment_ids).returning('id', 'status')) - end - - def self.reverse_disburse_all_with_payments(payment_ids) - Charge.where("payment_id IN (?)", payment_ids).update_all(status: 'available') - end + def self.reverse_disburse_all_with_payments(payment_ids) + Charge.where('payment_id IN (?)', payment_ids).update_all(status: 'available') + end end diff --git a/lib/update/update_custom_field_joins.rb b/lib/update/update_custom_field_joins.rb index ef59f0b6..7b755237 100644 --- a/lib/update/update_custom_field_joins.rb +++ b/lib/update/update_custom_field_joins.rb @@ -1,23 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module UpdateCustomFieldJoins - # Delete custom field joins that have the same custom field master # Favor the most recent custom field join def self.delete_dupes(supporter_ids) # Bulk remove duplicate custom field joins, favoring the most recent one ids = Qx.select('ARRAY_AGG(custom_field_joins.id ORDER BY custom_field_joins.created_at DESC) AS ids') - .from(:custom_field_joins) - .where("custom_field_joins.supporter_id IN ($ids)", ids: supporter_ids) - .join("custom_field_masters cfms", "cfms.id = custom_field_joins.custom_field_master_id") - .group_by("cfms.name") - .having("COUNT(custom_field_joins) > 1") - .execute.map{|h| h['ids'][1..-1]}.flatten + .from(:custom_field_joins) + .where('custom_field_joins.supporter_id IN ($ids)', ids: supporter_ids) + .join('custom_field_masters cfms', 'cfms.id = custom_field_joins.custom_field_master_id') + .group_by('cfms.name') + .having('COUNT(custom_field_joins) > 1') + .execute.map { |h| h['ids'][1..-1] }.flatten return unless ids.any? + Qx.delete_from(:custom_field_joins) - .where("id IN ($ids)", ids: ids) + .where('id IN ($ids)', ids: ids) .execute end - end diff --git a/lib/update/update_disputes.rb b/lib/update/update_disputes.rb index 8f73f79c..b33360d3 100644 --- a/lib/update/update_disputes.rb +++ b/lib/update/update_disputes.rb @@ -1,10 +1,11 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateDisputes - def self.disburse_all_with_payments(payment_ids) Psql.execute( - Qexpr.new.update(:disputes, {status: 'lost_and_paid'}).where("payment_id IN ($ids)", ids: payment_ids) + Qexpr.new.update(:disputes, status: 'lost_and_paid').where('payment_id IN ($ids)', ids: payment_ids) ) end end diff --git a/lib/update/update_donation.rb b/lib/update/update_donation.rb index ff680b82..15cb5150 100644 --- a/lib/update/update_donation.rb +++ b/lib/update/update_donation.rb @@ -1,77 +1,75 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateDonation - def self.from_followup(donation, params) donation.designation = params[:designation] if params[:designation].present? donation.dedication = params[:dedication] if params[:dedication].present? donation.comment = params[:comment] if params[:comment].present? donation.save - return donation + donation end # @param [Integer] donation_id the donation for the payment you wish to modify def self.update_payment(donation_id, data) - ParamValidation.new({id:donation_id, data: data}, - { - id: {required: true, is_reference: true}, - data: {required: true, is_hash: true} - }) + ParamValidation.new({ id: donation_id, data: data }, + id: { required: true, is_reference: true }, + data: { required: true, is_hash: true }) existing_payment = Payment.where('donation_id = ?', donation_id).last unless existing_payment raise ParamValidation::ValidationError.new("#{donation_id} is does not correspond to a valid donation", - {key: :id}) + key: :id) end is_offsite = !existing_payment.offsite_payment.nil? validations = { - designation: {is_a: String}, - dedication: {is_a: String}, - comment: {is_a: String}, - campaign_id: {is_reference: true, required:true}, - event_id: {is_reference: true, required: true} + designation: { is_a: String }, + dedication: { is_a: String }, + comment: { is_a: String }, + campaign_id: { is_reference: true, required: true }, + event_id: { is_reference: true, required: true } } if is_offsite # if offline test the other values (fee_total, gross_amount, check_number, date) # - validations.merge!({gross_amount: {is_integer: true, min: 1}, - fee_total: {is_integer: true}, - check_number: {is_a: String}, - date: {can_be_date: true}}) + validations.merge!(gross_amount: { is_integer: true, min: 1 }, + fee_total: { is_integer: true }, + check_number: { is_a: String }, + date: { can_be_date: true }) end ParamValidation.new(data, validations) - set_to_nil = {campaign: data[:campaign_id] == '', event: data[:event_id] == ''} + set_to_nil = { campaign: data[:campaign_id] == '', event: data[:event_id] == '' } # validate campaign and event ids if there and if they belong to nonprofit - if (set_to_nil[:campaign]) + if set_to_nil[:campaign] campaign = nil else campaign = Campaign.where('id = ?', data[:campaign_id]).first unless campaign - raise ParamValidation::ValidationError.new("#{data[:campaign_id]} is not a valid campaign", {key: :campaign_id}) + raise ParamValidation::ValidationError.new("#{data[:campaign_id]} is not a valid campaign", key: :campaign_id) end unless campaign.nonprofit == existing_payment.nonprofit - raise ParamValidation::ValidationError.new("#{data[:campaign_id]} campaign does not belong to this nonprofit for payment #{existing_payment.id}", {key: :campaign_id}) + raise ParamValidation::ValidationError.new("#{data[:campaign_id]} campaign does not belong to this nonprofit for payment #{existing_payment.id}", key: :campaign_id) end end - if (set_to_nil[:event]) + if set_to_nil[:event] event = nil else event = Event.where('id = ?', data[:event_id]).first unless event - raise ParamValidation::ValidationError.new("#{data[:event_id]} is not a valid event", {key: :event_id}) + raise ParamValidation::ValidationError.new("#{data[:event_id]} is not a valid event", key: :event_id) end unless event.nonprofit == existing_payment.nonprofit - raise ParamValidation::ValidationError.new("#{data[:event_id]} event does not belong to this nonprofit for payment #{existing_payment.id}", {key: :event_id}) + raise ParamValidation::ValidationError.new("#{data[:event_id]} event does not belong to this nonprofit for payment #{existing_payment.id}", key: :event_id) end end Qx.transaction do - donation = existing_payment.donation donation.designation = data[:designation] if data[:designation] @@ -87,47 +85,38 @@ module UpdateDonation donation.date = data[:date] if data[:date] end - # edits_to_payments if is_offsite - #if offline, set date, gross_amount, fee_total, net_amount + # if offline, set date, gross_amount, fee_total, net_amount existing_payment.towards = data[:designation] if data[:designation] existing_payment.date = data[:date] if data[:date] existing_payment.gross_amount = data[:gross_amount] if data[:gross_amount] existing_payment.fee_total = data[:fee_total] if data[:fee_total] existing_payment.net_amount = existing_payment.gross_amount - existing_payment.fee_total - if existing_payment.changed? - existing_payment.save! - end + existing_payment.save! if existing_payment.changed? else if donation.designation - Payment.where('donation_id = ?', donation.id).update_all(:towards => donation.designation, updated_at: Time.now) + Payment.where('donation_id = ?', donation.id).update_all(towards: donation.designation, updated_at: Time.now) end end - #if offsite, set check_number, date, gross_amount + # if offsite, set check_number, date, gross_amount if is_offsite offsite_payment = existing_payment.offsite_payment offsite_payment.check_number = data[:check_number] if data[:check_number] offsite_payment.date = data[:date] if data[:date] offsite_payment.gross_amount = data[:gross_amount] if data[:gross_amount] - if offsite_payment.changed? - offsite_payment.save! - end - end - if donation.changed? - donation.save! + offsite_payment.save! if offsite_payment.changed? end + donation.save! if donation.changed? existing_payment.reload ret = donation.attributes ret[:payment] = existing_payment.attributes - if is_offsite - ret[:offsite_payment] = offsite_payment.attributes - end + ret[:offsite_payment] = offsite_payment.attributes if is_offsite return ret end end @@ -140,16 +129,13 @@ module UpdateDonation donation.date = donation.created_at donation.save! - - payments = Payment.where('donation_id = ?', id).includes(:charge) - - payments.each {|p| + payments.each do |p| @payments_corrected.push(p.id) p.date = p.charge.created_at p.save! - } + end donation.save! @@ -159,14 +145,19 @@ module UpdateDonation def self.any_donations_with_created_at_after_date donation_ids = Set.new - CSV.foreach('bad_payments_2.csv').select {|row| true if Integer(row[0]) rescue false}.collect{|row| - is_int = true if Integer(row[0]) rescue false - if (is_int && Float(row[6]) > 0) - donation_ids.add(row[0]) + CSV.foreach('bad_payments_2.csv').select do |row| + true if Integer(row[0]) + rescue StandardError + false + end .collect do |row| + begin + is_int = true if Integer(row[0]) + rescue StandardError + false end - } + donation_ids.add(row[0]) if is_int && Float(row[6]) > 0 + end - return donation_ids + donation_ids end end - diff --git a/lib/update/update_email_lists.rb b/lib/update/update_email_lists.rb index 3f26035a..54dc3ddc 100644 --- a/lib/update/update_email_lists.rb +++ b/lib/update/update_email_lists.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module UpdateEmailLists - def self.populate_lists_on_mailchimp(npo_id) - lists = Qx.select("tag_master_id", "list_name", "mailchimp_list_id") - .from(:email_lists) - .where(nonprofit_id: npo_id) - .execute - post_data = Qx.select("supporters.email", "email_lists.mailchimp_list_id") - .from("email_lists") - .add_join("tag_masters", "tag_masters.id=email_lists.tag_master_id") - .add_join("tag_joins", "tag_joins.tag_master_id=tag_masters.id") - .add_join("supporters", "supporters.id=tag_joins.supporter_id") - .where("email_lists.nonprofit_id=$id", id: npo_id) - .execute - .map{|h| {method: 'POST', path: "lists/#{h['mailchimp_list_id']}/members", body: {email_address: h['email'], status: 'subscribed'}.to_json}} + lists = Qx.select('tag_master_id', 'list_name', 'mailchimp_list_id') + .from(:email_lists) + .where(nonprofit_id: npo_id) + .execute + post_data = Qx.select('supporters.email', 'email_lists.mailchimp_list_id') + .from('email_lists') + .add_join('tag_masters', 'tag_masters.id=email_lists.tag_master_id') + .add_join('tag_joins', 'tag_joins.tag_master_id=tag_masters.id') + .add_join('supporters', 'supporters.id=tag_joins.supporter_id') + .where('email_lists.nonprofit_id=$id', id: npo_id) + .execute + .map { |h| { method: 'POST', path: "lists/#{h['mailchimp_list_id']}/members", body: { email_address: h['email'], status: 'subscribed' }.to_json } } Mailchimp.perform_batch_operations(npo_id, post_data) end - end diff --git a/lib/update/update_email_settings.rb b/lib/update/update_email_settings.rb index 463a5cca..89956681 100644 --- a/lib/update/update_email_settings.rb +++ b/lib/update/update_email_settings.rb @@ -1,18 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateEmailSettings - def self.save(np_id, user_id, params) es = Psql.execute( Qexpr.new.select(:id).from(:email_settings) - .where("nonprofit_id=$id", id: np_id.to_i) - .where("user_id=$id", id: user_id) + .where('nonprofit_id=$id', id: np_id.to_i) + .where('user_id=$id', id: user_id) ).first if es.nil? - es = Psql.execute(Qexpr.new.insert('email_settings', [{nonprofit_id: np_id, user_id: user_id}], {no_timestamps: true})).first + es = Psql.execute(Qexpr.new.insert('email_settings', [{ nonprofit_id: np_id, user_id: user_id }], no_timestamps: true)).first end Psql.execute( Qexpr.new.update(:email_settings, params) - .where("id=$id", id: es['id']) + .where('id=$id', id: es['id']) .returning('*') ).first end diff --git a/lib/update/update_miscellaneous_np_info.rb b/lib/update/update_miscellaneous_np_info.rb index 1dae749d..2e0b37a6 100644 --- a/lib/update/update_miscellaneous_np_info.rb +++ b/lib/update/update_miscellaneous_np_info.rb @@ -1,23 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateMiscellaneousNpInfo def self.update(np_id, misc_settings) - ParamValidation.new({np_id: np_id, misc_settings:misc_settings}, - np_id: {:required => true, :is_integer => true}, - misc_settings: {:required => true, :is_hash => true}) + ParamValidation.new({ np_id: np_id, misc_settings: misc_settings }, + np_id: { required: true, is_integer: true }, + misc_settings: { required: true, is_hash: true }) np = Nonprofit.where('id = ?', np_id).first - raise ParamValidation::ValidationError.new("Nonprofit #{np_id} does not exist", {key: :np_id}) unless np + raise ParamValidation::ValidationError.new("Nonprofit #{np_id} does not exist", key: :np_id) unless np + misc = MiscellaneousNpInfo.where('nonprofit_id = ?', np_id).first unless misc misc = MiscellaneousNpInfo.new misc.nonprofit = np end - if (misc_settings[:donate_again_url].present?) + if misc_settings[:donate_again_url].present? misc.donate_again_url = misc_settings[:donate_again_url] end - if (misc_settings[:change_amount_message].present?) - if (Format::HTML.has_only_empty_tags(misc_settings[:change_amount_message])) - misc.change_amount_message= nil; + if misc_settings[:change_amount_message].present? + if Format::HTML.has_only_empty_tags(misc_settings[:change_amount_message]) + misc.change_amount_message = nil else misc.change_amount_message = misc_settings[:change_amount_message] end @@ -26,4 +29,4 @@ module UpdateMiscellaneousNpInfo misc.save! misc end -end \ No newline at end of file +end diff --git a/lib/update/update_nonprofit.rb b/lib/update/update_nonprofit.rb index 78861949..4d890628 100644 --- a/lib/update/update_nonprofit.rb +++ b/lib/update/update_nonprofit.rb @@ -1,11 +1,12 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'query/query_payments' require 'fetch/stripe/fetch_stripe_account' module UpdateNonprofit - # See the stripe docs for reference: => https://stripe.com/docs/connect/identity-verification - def self.verify_identity(np_id, legal_entity, tos=nil) - np = Qx.select("*").from(:nonprofits).where(id: np_id).execute.first + def self.verify_identity(np_id, legal_entity, tos = nil) + np = Qx.select('*').from(:nonprofits).where(id: np_id).execute.first legal_entity[:address][:country] = 'US' if legal_entity[:address] acct = FetchStripeAccount.with_account_id(np['stripe_account_id']) acct.legal_entity.phone_number = acct.support_phone = legal_entity[:phone_number] if legal_entity[:phone_number] @@ -18,48 +19,45 @@ module UpdateNonprofit acct.legal_entity.personal_id_number = legal_entity[:personal_id_number] if legal_entity[:personal_id_number] acct.legal_entity.type = 'company' acct.legal_entity.business_name = np['name'] - if tos - acct.tos_acceptance = tos - end + acct.tos_acceptance = tos if tos acct.save # Might as well update the nonprofit info if legal_entity[:address] && legal_entity[:business_tax_id] - np = Qx.update(:nonprofits).set({ - address: legal_entity[:address][:line1], - city: legal_entity[:address][:city], - state_code: legal_entity[:address][:state], - zip_code: legal_entity[:address][:postal_code], - ein: legal_entity[:business_tax_id], - verification_status: 'pending', - phone: legal_entity[:phone_number] - }) - .where(id: np_id) - .returning('*') - .execute.first + np = Qx.update(:nonprofits).set( + address: legal_entity[:address][:line1], + city: legal_entity[:address][:city], + state_code: legal_entity[:address][:state], + zip_code: legal_entity[:address][:postal_code], + ein: legal_entity[:business_tax_id], + verification_status: 'pending', + phone: legal_entity[:phone_number] + ) + .where(id: np_id) + .returning('*') + .execute.first else np = Qx.update(:nonprofits).set(verification_status: 'pending').where(id: np_id).returning('*').first end - return np + np end - # Update charges from pending to available if the nonprofit's balance on stripe can accommodate them # First, get net balance on Stripe, then get net balance on CC # Take the difference of those two, and mark as many oldest pending charges as 'available' as are less than or equal to that difference def self.mark_available_charges(npo_id) - stripe_account_id = Qx.select("stripe_account_id").from(:nonprofits).where(id: npo_id).ex.first['stripe_account_id'] + stripe_account_id = Qx.select('stripe_account_id').from(:nonprofits).where(id: npo_id).ex.first['stripe_account_id'] stripe_net_balance = Stripe::Balance.retrieve(stripe_account: stripe_account_id).available.first.amount cc_net_balance = QueryPayments.get_payout_totals(QueryPayments.ids_for_payout(npo_id))['net_amount'] - pending_payments = Qx.select("payments.net_amount", "charges.id AS charge_id") - .from(:payments) - .where("charges.status='pending'") - .and_where("payments.nonprofit_id=$id", id: npo_id) - .join("charges", "charges.payment_id=payments.id") - .order_by("payments.date ASC") - .execute + pending_payments = Qx.select('payments.net_amount', 'charges.id AS charge_id') + .from(:payments) + .where("charges.status='pending'") + .and_where('payments.nonprofit_id=$id', id: npo_id) + .join('charges', 'charges.payment_id=payments.id') + .order_by('payments.date ASC') + .execute return if pending_payments.empty? @@ -69,10 +67,8 @@ module UpdateNonprofit remaining_balance -= payment['net_amount'] true end - end.map{|h| h['charge_id']} + end.map { |h| h['charge_id'] } - Qx.update(:charges).set(status: 'available').where("id IN ($ids)", ids: charge_ids).execute if charge_ids.any? + Qx.update(:charges).set(status: 'available').where('id IN ($ids)', ids: charge_ids).execute if charge_ids.any? end - end - diff --git a/lib/update/update_order.rb b/lib/update/update_order.rb index 23738859..4936da86 100644 --- a/lib/update/update_order.rb +++ b/lib/update/update_order.rb @@ -1,15 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module UpdateOrder - # data is an array of hashes of: # - id : id of row to update - # - order: new order of row to update + # - order: new order of row to update def self.with_data(table_name, data) - vals = data.map{|h| "(#{h[:id].to_i}, #{h[:order].to_i})"}.join(", ") + vals = data.map { |h| "(#{h[:id].to_i}, #{h[:order].to_i})" }.join(', ') from_str = "(VALUES #{vals}) AS data(id, \"order\")" - return Qx.update("#{table_name}") + Qx.update(table_name.to_s) .set('"order"="data"."order"') .timestamps .from(from_str) @@ -17,6 +18,4 @@ module UpdateOrder .returning("#{table_name}.order", "#{table_name}.id") .execute end - end - diff --git a/lib/update/update_payouts.rb b/lib/update/update_payouts.rb index 37a795fa..257523f7 100644 --- a/lib/update/update_payouts.rb +++ b/lib/update/update_payouts.rb @@ -1,21 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdatePayouts - def self.reverse_with_stripe(payout_id, status, failure_message) - ParamValidation.new({payout_id:payout_id, status: status, failure_message: failure_message}, { - payout_id: {required: true, is_integer: true}, - status: {included_in:['pending', 'paid', 'canceled', 'failed'], required: true}, - failure_message: {not_blank: true, required: true} - }) + ParamValidation.new({ payout_id: payout_id, status: status, failure_message: failure_message }, + payout_id: { required: true, is_integer: true }, + status: { included_in: %w[pending paid canceled failed], required: true }, + failure_message: { not_blank: true, required: true }) payout = Payout.where('id = ?', payout_id).first unless payout - raise ParamValidation::ValidationError.new("No payout with id number: #{payout_id} ", [{key: :payout_id}]) + raise ParamValidation::ValidationError.new("No payout with id number: #{payout_id} ", [{ key: :payout_id }]) end - payment_ids = payout.payments.select('payments.id').map{|i| i.id}.to_a + payment_ids = payout.payments.select('payments.id').map(&:id).to_a if payment_ids.count < 1 - raise ArgumentError.new("No payments are available to reverse.") + raise ArgumentError, 'No payments are available to reverse.' end + now = Time.current Psql.transaction do @@ -26,16 +27,15 @@ module UpdatePayouts UpdateRefunds.reverse_disburse_all_with_payments(payment_ids) # Mark all disputes as lost_and_paid - #UpdateDisputes.disburse_all_with_payments(payment_ids) + # UpdateDisputes.disburse_all_with_payments(payment_ids) # Get gross total, total fees, net total, and total count # Create the payout record (whether it succeeded on Stripe or not) payout.status = status payout.failure_message = failure_message payout.save! - - #NonprofitMailer.delay.pending_payout_notification(payout['id'].to_i) + # NonprofitMailer.delay.pending_payout_notification(payout['id'].to_i) payout end end -end \ No newline at end of file +end diff --git a/lib/update/update_recurring_donations.rb b/lib/update/update_recurring_donations.rb index 7fb21985..2047b304 100644 --- a/lib/update/update_recurring_donations.rb +++ b/lib/update/update_recurring_donations.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'query/query_recurring_donations' require 'insert/insert_supporter_notes' @@ -5,61 +7,55 @@ require 'format/date' require 'format/currency' module UpdateRecurringDonations - # Update the card id and name for a given recurring donation (provide rd['donation_id']) def self.update_card_id(rd, token) rd = rd&.with_indifferent_access - ParamValidation.new({rd: rd, token: token}, - { - rd: {is_hash: true, required: true}, - token: {format: UUID::Regex, required: true} - }) + ParamValidation.new({ rd: rd, token: token }, + rd: { is_hash: true, required: true }, + token: { format: UUID::Regex, required: true }) ParamValidation.new(rd, - { - id: {is_reference: true, required: true} - }) + id: { is_reference: true, required: true }) source_token = QuerySourceToken.get_and_increment_source_token(token, nil) tokenizable = source_token.tokenizable - - entities = RetrieveActiveRecordItems.retrieve_from_keys(rd, RecurringDonation => :id ) + entities = RetrieveActiveRecordItems.retrieve_from_keys(rd, RecurringDonation => :id) validate_entities(entities[:id], tokenizable) Qx.transaction do rec_don = entities[:id] donation = rec_don.donation - #TODO This is stupid but the two are used together inconsistently. We should scrap one or the other. + # TODO: This is stupid but the two are used together inconsistently. We should scrap one or the other. donation.card = tokenizable rec_don.card_id = tokenizable rec_don.n_failures = 0 rec_don.save! donation.save! - InsertSupporterNotes.create([{content: "This supporter updated their card for their recurring donation with ID #{rec_don.id}", supporter_id: rec_don.supporter.id, user_id: 540}]) + InsertSupporterNotes.create([{ content: "This supporter updated their card for their recurring donation with ID #{rec_don.id}", supporter_id: rec_don.supporter.id, user_id: 540 }]) end - return QueryRecurringDonations.fetch_for_edit(rd[:id])['recurring_donation'] + QueryRecurringDonations.fetch_for_edit(rd[:id])['recurring_donation'] end # Update the paydate for a given recurring donation (provide rd['id']) def self.update_paydate(rd, paydate) - return ValidationError.new(['Invalid paydate']) unless (1..28).include?(paydate.to_i) - Psql.execute(Qexpr.new.update(:recurring_donations, paydate: paydate).where("id=$id", id: rd['id'])) + return ValidationError.new(['Invalid paydate']) unless (1..28).cover?(paydate.to_i) + + Psql.execute(Qexpr.new.update(:recurring_donations, paydate: paydate).where('id=$id', id: rd['id'])) rd['paydate'] = paydate - return rd + rd end # @param [RecurringDonation] rd # @param [String] token # @param [Integer] amount def self.update_amount(rd, token, amount) - ParamValidation.new({amount: amount, rd: rd, token: token}, - {amount: {is_integer: true, min: 50, required:true}, - rd: {required:true, is_a: RecurringDonation}, - token: {required:true, format: UUID::Regex} - }) + ParamValidation.new({ amount: amount, rd: rd, token: token }, + amount: { is_integer: true, min: 50, required: true }, + rd: { required: true, is_a: RecurringDonation }, + token: { required: true, format: UUID::Regex }) source_token = QuerySourceToken.get_and_increment_source_token(token, nil) tokenizable = source_token.tokenizable @@ -68,63 +64,58 @@ module UpdateRecurringDonations previous_amount = rd.amount donation = rd.donation Qx.transaction do - #TODO This is stupid but the two are used together inconsistently. We should scrap one or the other. + # TODO: This is stupid but the two are used together inconsistently. We should scrap one or the other. rd.card = tokenizable rd.amount = amount - rd.n_failures= 0 + rd.n_failures = 0 donation.card = tokenizable donation.amount = amount rd.save! donation.save! end EmailJobQueue.queue(JobTypes::NonprofitRecurringDonationChangeAmountJob, rd.id, previous_amount) - EmailJobQueue.queue(JobTypes::DonorRecurringDonationChangeAmountJob,rd.id, previous_amount) + EmailJobQueue.queue(JobTypes::DonorRecurringDonationChangeAmountJob, rd.id, previous_amount) rd end - def self.update_from_start_dates - RecurringDonation.inactive.where("start_date >= ?", Date.today).update_all(active: true) + RecurringDonation.inactive.where('start_date >= ?', Date.today).update_all(active: true) end - def self.update_from_end_dates - RecurringDonation.active.where("end_date < ?", Date.today).update_all(active: false) + RecurringDonation.active.where('end_date < ?', Date.today).update_all(active: false) end - # Cancel a recurring donation (set active='f') and record the supporter/user email who did it - def self.cancel(rd_id, email, dont_notify_nonprofit=false) + def self.cancel(rd_id, email, dont_notify_nonprofit = false) Psql.execute( - Qexpr.new.update(:recurring_donations, { - active: false, - cancelled_by: email, - cancelled_at: Time.current - }) - .where("id=$id", id: rd_id.to_i) + Qexpr.new.update(:recurring_donations, + active: false, + cancelled_by: email, + cancelled_at: Time.current) + .where('id=$id', id: rd_id.to_i) ) rd = QueryRecurringDonations.fetch_for_edit(rd_id)['recurring_donation'] - InsertSupporterNotes.create([{supporter_id: rd['supporter_id'], content: "This supporter's recurring donation for $#{Format::Currency.cents_to_dollars(rd['amount'])} was cancelled by #{rd['cancelled_by']} on #{Format::Date.simple(rd['cancelled_at'])}", user_id: 540}]) - if (!dont_notify_nonprofit) + InsertSupporterNotes.create([{ supporter_id: rd['supporter_id'], content: "This supporter's recurring donation for $#{Format::Currency.cents_to_dollars(rd['amount'])} was cancelled by #{rd['cancelled_by']} on #{Format::Date.simple(rd['cancelled_at'])}", user_id: 540 }]) + unless dont_notify_nonprofit DonationMailer.delay.nonprofit_recurring_donation_cancellation(rd['donation_id']) end - return rd + rd end - def self.update(rd, params) params = set_defaults(params) if params[:donation] rd.donation.update_attributes(params[:donation]) return rd.donation unless rd.donation.valid? + params = params.except(:donation) end rd.update_attributes(params) - return rd + rd end - def self.set_defaults(params) if params[:donation] && params[:donation][:dollars] params[:donation][:amount] = Format::Currency.dollars_to_cents(params[:donation][:dollars]) @@ -141,17 +132,17 @@ module UpdateRecurringDonations params = params.except(:end_date_str) end - return params + params end # @param [RecurringDonation] rd # @param [Card] tokenizable def self.validate_entities(rd, tokenizable) - if (rd.cancelled_at) + if rd.cancelled_at raise ParamValidation::ValidationError.new("Recurring Donation #{rd.id} is already cancelled.", key: :id) end - if (tokenizable.deleted) + if tokenizable.deleted raise ParamValidation::ValidationError.new("Tokenized card #{tokenizable.id} is not valid.", key: :token) end @@ -159,6 +150,4 @@ module UpdateRecurringDonations raise ParamValidation::ValidationError.new("Supporter #{rd.supporter.id} does not own card #{tokenizable.id}", key: :token) end end - end - diff --git a/lib/update/update_refunds.rb b/lib/update/update_refunds.rb index c04cf048..f03f83b5 100644 --- a/lib/update/update_refunds.rb +++ b/lib/update/update_refunds.rb @@ -1,16 +1,17 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateRefunds - - def self.disburse_all_with_payments(payment_ids) + def self.disburse_all_with_payments(payment_ids) expr = Qx.update(:refunds) - .set(disbursed: true) - .timestamps - .where("payment_id IN ($ids)", ids: payment_ids) - .returning('*') - .execute - end + .set(disbursed: true) + .timestamps + .where('payment_id IN ($ids)', ids: payment_ids) + .returning('*') + .execute + end - def self.reverse_disburse_all_with_payments(payment_ids) - Refund.where("payment_id IN (?)", payment_ids).update_all(disbursed:false) - end + def self.reverse_disburse_all_with_payments(payment_ids) + Refund.where('payment_id IN (?)', payment_ids).update_all(disbursed: false) + end end diff --git a/lib/update/update_supporter.rb b/lib/update/update_supporter.rb index c74e379d..37addd8e 100644 --- a/lib/update/update_supporter.rb +++ b/lib/update/update_supporter.rb @@ -1,18 +1,19 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateSupporter - - def self.from_info(supporter, params) - supporter.update_attributes(params) - #GeocodeModel.delay.geocode(supporter) - return supporter - end + def self.from_info(supporter, params) + supporter.update_attributes(params) + # GeocodeModel.delay.geocode(supporter) + supporter + end # Bulk delete, meaning mark all supporters given by a query as deleted='t' def self.bulk_delete(np_id, supporter_ids) Qx.update(:supporters) .set(deleted: true) - .where("id IN ($ids)", ids: supporter_ids) - .and_where("nonprofit_id=$id", id: np_id) + .where('id IN ($ids)', ids: supporter_ids) + .and_where('nonprofit_id=$id', id: np_id) .returning('id') .execute end @@ -20,5 +21,4 @@ module UpdateSupporter def self.general_info(supporter_id, data) Qx.update(:supporters).set(data).where(id: supporter_id).returning('*').ex.last end - end diff --git a/lib/update/update_supporter_notes.rb b/lib/update/update_supporter_notes.rb index 0c0cdd41..99cfbe1c 100644 --- a/lib/update/update_supporter_notes.rb +++ b/lib/update/update_supporter_notes.rb @@ -1,8 +1,9 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'qx' module UpdateSupporterNotes - def self.update(note) Qx.update(:supporter_notes) .set(content: note[:content], user_id: note[:user_id]) @@ -10,17 +11,15 @@ module UpdateSupporterNotes .where(id: note[:id]) .execute UpdateActivities.for_supporter_notes(note) - end + end # sets the deleted column to true on supporter_notes (soft delete) - # and then does a hard delete on the associated activity + # and then does a hard delete on the associated activity def self.delete(id) Qx.update(:supporter_notes) .set(deleted: true) .where(id: id) .execute Qx.delete_from(:activities).where(attachment_id: id).execute - end - + end end - diff --git a/lib/update/update_tickets.rb b/lib/update/update_tickets.rb index 393ba2a9..728c4aeb 100644 --- a/lib/update/update_tickets.rb +++ b/lib/update/update_tickets.rb @@ -1,17 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateTickets + def self.update(data, current_user = nil) + ParamValidation.new(data, + event_id: { required: true, is_reference: true }, + ticket_id: { required: true, is_reference: true }, + token: { format: UUID::Regex }, + bid_id: { is_integer: true }, + # note: nothing to check? - def self.update(data, current_user=nil) - ParamValidation.new(data, { - event_id: {required:true, is_reference: true}, - ticket_id: {required: true, is_reference: true}, - token: {format: UUID::Regex}, - bid_id: {is_integer: true}, - #note: nothing to check? - - checked_in: {included_in: ['true', 'false', true, false]} - - }) + checked_in: { included_in: ['true', 'false', true, false] }) entities = RetrieveActiveRecordItems.retrieve_from_keys(data, Event => :event_id, Ticket => :ticket_id) validate_entities(entities) @@ -78,21 +77,21 @@ module UpdateTickets return { json: { error: "No ticket with id #{ticket_id} at event with id #{event_id}\n #{e.message}" }, status: :unprocessable_entity } rescue ActiveRecord::ActiveRecordError - return { json: { error: "There was a DB error. Please contact support" }, + return { json: { error: 'There was a DB error. Please contact support' }, status: :unprocessable_entity } end end def self.validate_entities(entities) - if (entities[:ticket_id].deleted) + if entities[:ticket_id].deleted raise ParamValidation::ValidationError.new("Ticket ID #{entities[:ticket_id].id} is deleted", key: :ticket_id) end - if (entities[:event_id].deleted) + if entities[:event_id].deleted raise ParamValidation::ValidationError.new("Event ID #{entities[:event_id].id} is deleted", key: :event_id) end - if (entities[:ticket_id].event != entities[:event_id]) + if entities[:ticket_id].event != entities[:event_id] raise ParamValidation::ValidationError.new("Ticket ID #{entities[:ticket_id].id} does not belong to event #{entities[:event_id].id}", key: :ticket_id) end end diff --git a/lib/uuid.rb b/lib/uuid.rb index 4fb82fe5..2bd69d34 100644 --- a/lib/uuid.rb +++ b/lib/uuid.rb @@ -1,4 +1,6 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UUID - Regex = /\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?/ -end \ No newline at end of file + Regex = /\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?/.freeze +end diff --git a/lib/validation_error.rb b/lib/validation_error.rb index 1cf68a91..2d5f9aa0 100644 --- a/lib/validation_error.rb +++ b/lib/validation_error.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # A generalized, all purpose struct for database validation errors # .errors is simply array of error messages diff --git a/script/delayed_job b/script/delayed_job index edf19598..4f1b0b2b 100755 --- a/script/delayed_job +++ b/script/delayed_job @@ -1,4 +1,5 @@ #!/usr/bin/env ruby +# frozen_string_literal: true require File.expand_path(File.join(File.dirname(__FILE__), '..', 'config', 'environment')) require 'delayed/command' diff --git a/script/rails b/script/rails index c5b1430b..12b00eff 100755 --- a/script/rails +++ b/script/rails @@ -1,11 +1,13 @@ #!/usr/bin/env ruby +# frozen_string_literal: true + # This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application. if ENV['RAILS_ENV'] == 'test' require 'simplecov' SimpleCov.start 'rails' - puts "required simplecov" + puts 'required simplecov' end -APP_PATH = File.expand_path('../../config/application', __FILE__) -require File.expand_path('../../config/boot', __FILE__) +APP_PATH = File.expand_path('../config/application', __dir__) +require File.expand_path('../config/boot', __dir__) require 'rails/commands' diff --git a/spec/api/houdini/nonprofit_spec.rb b/spec/api/houdini/nonprofit_spec.rb index 396cf775..29d84555 100644 --- a/spec/api/houdini/nonprofit_spec.rb +++ b/spec/api/houdini/nonprofit_spec.rb @@ -1,26 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' -describe Houdini::V1::Nonprofit, :type => :request do +describe Houdini::V1::Nonprofit, type: :request do describe 'get' do - end describe 'post' do around(:each) do |example| - @old_bp =Settings.default_bp + @old_bp = Settings.default_bp example.run Settings.default_bp = @old_bp - end def expect_validation_errors(actual, input) expected_errors = input.with_indifferent_access[:errors] - expect(actual["errors"]).to match_array expected_errors + expect(actual['errors']).to match_array expected_errors end def create_errors(*wrapper_params) output = totally_empty_errors - wrapper_params.each {|i| output[:errors].push(h(params: [i], messages: gr_e('presence')))} + wrapper_params.each { |i| output[:errors].push(h(params: [i], messages: gr_e('presence'))) } output end @@ -28,57 +28,57 @@ describe Houdini::V1::Nonprofit, :type => :request do h.with_indifferent_access end - let(:totally_empty_errors) { + let(:totally_empty_errors) do { errors: [ - h(params: ["nonprofit[name]"], messages: gr_e("presence", "blank")), - h(params: ["nonprofit[zip_code]"], messages: gr_e("presence", "blank")), - h(params: ["nonprofit[state_code]"], messages: gr_e("presence", "blank")), - h(params: ["nonprofit[city]"], messages: gr_e("presence", "blank")), + h(params: ['nonprofit[name]'], messages: gr_e('presence', 'blank')), + h(params: ['nonprofit[zip_code]'], messages: gr_e('presence', 'blank')), + h(params: ['nonprofit[state_code]'], messages: gr_e('presence', 'blank')), + h(params: ['nonprofit[city]'], messages: gr_e('presence', 'blank')), - h(params: ["user[name]"], messages: gr_e("presence", "blank")), - h(params: ["user[email]"], messages: gr_e("presence", "blank")), - h(params: ["user[password]"], messages: gr_e("presence", "blank")), - h(params: ["user[password_confirmation]"], messages: gr_e("presence", "blank")), + h(params: ['user[name]'], messages: gr_e('presence', 'blank')), + h(params: ['user[email]'], messages: gr_e('presence', 'blank')), + h(params: ['user[password]'], messages: gr_e('presence', 'blank')), + h(params: ['user[password_confirmation]'], messages: gr_e('presence', 'blank')) ] - }.with_indifferent_access - } + end describe 'authorization' do - around(:each) {|e| + around(:each) do |e| Rails.configuration.action_controller.allow_forgery_protection = true e.run Rails.configuration.action_controller.allow_forgery_protection = false - } + end it 'rejects csrf' do post '/api/v1/nonprofit', params: {}, xhr: true - expect(response.code).to eq "401" + expect(response.code).to eq '401' end end it 'validates nothing' do input = {} post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "400" - expect_validation_errors(JSON.parse(response.body), create_errors("nonprofit", "user")) + expect(response.code).to eq '400' + expect_validation_errors(JSON.parse(response.body), create_errors('nonprofit', 'user')) end it 'validates url, email, phone ' do input = { - nonprofit: { - email: "noemeila", - phone: "notphone", - url: "" - }} + nonprofit: { + email: 'noemeila', + phone: 'notphone', + url: '' + } + } post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "400" - expected = create_errors("user") - expected[:errors].push(h(params:["nonprofit[email]"], messages: gr_e("regexp"))) - #expected[:errors].push(h(params:["nonprofit[phone]"], messages: gr_e("regexp"))) - #expected[:errors].push(h(params:["nonprofit[url]"], messages: gr_e("regexp"))) + expect(response.code).to eq '400' + expected = create_errors('user') + expected[:errors].push(h(params: ['nonprofit[email]'], messages: gr_e('regexp'))) + # expected[:errors].push(h(params:["nonprofit[phone]"], messages: gr_e("regexp"))) + # expected[:errors].push(h(params:["nonprofit[url]"], messages: gr_e("regexp"))) expect_validation_errors(JSON.parse(response.body), expected) end @@ -86,89 +86,84 @@ describe Houdini::V1::Nonprofit, :type => :request do it 'should reject unmatching passwords ' do input = { - user: { - email: "wmeil@email.com", - name: "name", - password: 'password', - password_confirmation: 'doesn\'t match' - } + user: { + email: 'wmeil@email.com', + name: 'name', + password: 'password', + password_confirmation: 'doesn\'t match' + } } post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "400" - expect(JSON.parse(response.body)['errors']).to include(h(params:["user[password]", "user[password_confirmation]"], messages: gr_e("is_equal_to"))) - + expect(response.code).to eq '400' + expect(JSON.parse(response.body)['errors']).to include(h(params: ['user[password]', 'user[password_confirmation]'], messages: gr_e('is_equal_to'))) end it 'attempts to make a slug copy and returns the proper errors' do - force_create(:nonprofit, slug: "n", state_code_slug: "wi", city_slug: "appleton") + force_create(:nonprofit, slug: 'n', state_code_slug: 'wi', city_slug: 'appleton') input = { - nonprofit: {name: "n", state_code: "WI", city: "appleton", zip_code: 54915}, - user: {name: "Name", email: "em@em.com", password: "12345678", password_confirmation: "12345678"} + nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915 }, + user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' } } expect_any_instance_of(SlugNonprofitNamingAlgorithm).to receive(:create_copy_name).and_raise(UnableToCreateNameCopyError.new) post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "400" + expect(response.code).to eq '400' - expect_validation_errors(JSON.parse(response.body), { - errors: [ - h( - params:["nonprofit[name]"], - messages:["has an invalid slug. Contact support for help."] - ) - ] - }) + expect_validation_errors(JSON.parse(response.body), + errors: [ + h( + params: ['nonprofit[name]'], + messages: ['has an invalid slug. Contact support for help.'] + ) + ]) end it 'errors on attempt to add user with email that already exists' do force_create(:user, email: 'em@em.com') input = { - nonprofit: {name: "n", state_code: "WI", city: "appleton", zip_code: 54915}, - user: {name: "Name", email: "em@em.com", password: "12345678", password_confirmation: "12345678"} + nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915 }, + user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' } } post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "400" - - expect_validation_errors(JSON.parse(response.body), { - errors: [ - h( - params:["user[email]"], - messages:["has already been taken"] - ) - ] - }) - + expect(response.code).to eq '400' + expect_validation_errors(JSON.parse(response.body), + errors: [ + h( + params: ['user[email]'], + messages: ['has already been taken'] + ) + ]) end - it "succeeds" do - force_create(:nonprofit, slug: "n", state_code_slug: "wi", city_slug: "appleton") + it 'succeeds' do + force_create(:nonprofit, slug: 'n', state_code_slug: 'wi', city_slug: 'appleton') input = { - nonprofit: {name: "n", state_code: "WI", city: "appleton", zip_code: 54915, url: 'www.cs.c', website: 'www.cs.c'}, - user: {name: "Name", email: "em@em.com", password: "12345678", password_confirmation: "12345678"} + nonprofit: { name: 'n', state_code: 'WI', city: 'appleton', zip_code: 54_915, url: 'www.cs.c', website: 'www.cs.c' }, + user: { name: 'Name', email: 'em@em.com', password: '12345678', password_confirmation: '12345678' } } bp = force_create(:billing_plan) Settings.default_bp.id = bp.id - #expect(Houdini::V1::Nonprofit).to receive(:sign_in) + # expect(Houdini::V1::Nonprofit).to receive(:sign_in) post '/api/v1/nonprofit', params: input, xhr: true - expect(response.code).to eq "201" + expect(response.code).to eq '201' our_np = Nonprofit.all[1] expected_np = { - name: "n", - state_code: "WI", - city: "appleton", - zip_code: "54915", - state_code_slug: "wi", - city_slug: "appleton", - slug: "n-00", - website: 'http://www.cs.c' + name: 'n', + state_code: 'WI', + city: 'appleton', + zip_code: '54915', + state_code_slug: 'wi', + city_slug: 'appleton', + slug: 'n-00', + website: 'http://www.cs.c' }.with_indifferent_access expected_np = our_np.attributes.with_indifferent_access.merge(expected_np) @@ -177,39 +172,33 @@ describe Houdini::V1::Nonprofit, :type => :request do expect(our_np.billing_subscription.billing_plan).to eq bp response_body = { - id: our_np.id + id: our_np.id }.with_indifferent_access expect(JSON.parse(response.body)).to eq response_body user = User.first expected_user = { - email: "em@em.com", - name: "Name" + email: 'em@em.com', + name: 'Name' } expected_user = user.attributes.with_indifferent_access.merge(expected_user) expect(our_np.roles.nonprofit_admins.count).to eq 1 expect(our_np.roles.nonprofit_admins.first.user.attributes).to eq expected_user - - end - - end end - def find_error_message(json, field_name) errors = json['errors'] - error = errors.select {|i| i["params"].any? {|j| j == field_name}}.first - return error if !error - return error["messages"] + error = errors.select { |i| i['params'].any? { |j| j == field_name } }.first + return error unless error + error['messages'] end def gr_e(*keys) - keys.map {|i| I18n.translate("grape.errors.messages." + i, locale: 'en')} - + keys.map { |i| I18n.translate('grape.errors.messages.' + i, locale: 'en') } end diff --git a/spec/api/support/api_shared_user_verification.rb b/spec/api/support/api_shared_user_verification.rb index ade50170..715bd6a8 100644 --- a/spec/api/support/api_shared_user_verification.rb +++ b/spec/api/support/api_shared_user_verification.rb @@ -1,61 +1,61 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'controllers/support/general_shared_user_context' RSpec.shared_context :api_shared_user_verification do include_context :general_shared_user_context - let(:user_as_np_admin) { + let(:user_as_np_admin) do __create_admin(nonprofit) - } + end - let(:user_as_other_np_admin) { + let(:user_as_other_np_admin) do __create_admin(other_nonprofit) - } + end - let(:user_as_np_associate){ + let(:user_as_np_associate) do __create_associate(nonprofit) - } + end - let(:user_as_other_np_associate){ + let(:user_as_other_np_associate) do __create_associate(other_nonprofit) - } + end - let(:unauth_user) { + let(:unauth_user) do force_create(:user) - } + end - - let(:campaign_editor) { + let(:campaign_editor) do __create(:campaign_editor, campaign) - } + end - let(:confirmed_user){ + let(:confirmed_user) do force_create(:user, confirmed_at: Time.current) - } + end - let(:event_editor) { - __create(:event_editor,event) - } + let(:event_editor) do + __create(:event_editor, event) + end - let(:super_admin) { + let(:super_admin) do __create(:super_admin, other_nonprofit) - } + end - let(:user_with_profile) { + let(:user_with_profile) do u = force_create(:user) force_create(:profile, user: u) u - } + end let(:all_users) do - {:user_as_np_admin => user_as_np_admin, - :user_as_other_np_admin => user_as_other_np_admin, - :user_as_np_associate => user_as_np_associate, - :user_as_other_np_associate => user_as_other_np_associate, - :unauth_user => unauth_user, - :campaign_editor => campaign_editor, - :event_editor => event_editor, - :super_admin => super_admin, - :user_with_profile => user_with_profile - } + { user_as_np_admin: user_as_np_admin, + user_as_other_np_admin: user_as_other_np_admin, + user_as_np_associate: user_as_np_associate, + user_as_other_np_associate: user_as_other_np_associate, + unauth_user: unauth_user, + campaign_editor: campaign_editor, + event_editor: event_editor, + super_admin: super_admin, + user_with_profile: user_with_profile } end let(:roles__open_to_all) do @@ -67,40 +67,37 @@ RSpec.shared_context :api_shared_user_verification do :campaign_editor, :event_editor, :super_admin, - :user_with_profile - ] + :user_with_profile] end let(:roles__open_to_np_associate) do - [:user_as_np_admin, + %i[user_as_np_admin - :user_as_np_associate, + user_as_np_associate - :super_admin - - ] + super_admin] end def __create(name, host) u = force_create(:user) - force_create(:role, user: u, name: name, host:host) + force_create(:role, user: u, name: name, host: host) u end def __create_admin(host) u = force_create(:user) - force_create(:role, user: u, name: :nonprofit_admin, host:host) + force_create(:role, user: u, name: :nonprofit_admin, host: host) u end def __create_associate(host) u = force_create(:user) - force_create(:role, user: u, name: :nonprofit_associate, host:host) + force_create(:role, user: u, name: :nonprofit_associate, host: host) u end def sign_in(user_to_signin) - post_via_redirect 'users/sign_in', 'user[email]' => user_to_signin.email, 'user[password]' => user_to_signin.password, format: "json" + post_via_redirect 'users/sign_in', 'user[email]' => user_to_signin.email, 'user[password]' => user_to_signin.password, format: 'json' end def sign_out @@ -110,38 +107,37 @@ RSpec.shared_context :api_shared_user_verification do def send(method, *args) case method when :get - return xhr(:get, *args) + xhr(:get, *args) when :post - return xhr(:post, *args) + xhr(:post, *args) when :delete - return xhr(:delete, *args) + xhr(:delete, *args) when :put - return xhr(:put, *args) + xhr(:put, *args) end end def accept(user_to_signin:, method:, action:, args:) new_user = user_to_signin - if (user_to_signin != nil && user_to_signin.is_a?(OpenStruct)) + if !user_to_signin.nil? && user_to_signin.is_a?(OpenStruct) new_user = user_to_signin.value end sign_in new_user if new_user # allows us to run the helpers but ignore what the controller action does # send(method, action, args) - expect(response.status).to eq(200), "expcted success for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ":" : "")} #{new_user&.attributes}" + expect(response.status).to eq(200), "expcted success for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ':' : '')} #{new_user&.attributes}" sign_out end def reject(user_to_signin:, method:, action:, args:) - new_user = user_to_signin - if (user_to_signin != nil && user_to_signin.is_a?(OpenStruct)) + if !user_to_signin.nil? && user_to_signin.is_a?(OpenStruct) new_user = user_to_signin.value end sign_in new_user if new_user send(method, action, args) - expect(response.status).to eq(401), "expected failure for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ":" : "")} #{new_user&.attributes}" + expect(response.status).to eq(401), "expected failure for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ':' : '')} #{new_user&.attributes}" sign_out end @@ -151,29 +147,26 @@ RSpec.shared_context :api_shared_user_verification do @method = details[:method] @successful_users = details[:successful_users] @action = details[:action] - @block_to_get_arguments_to_run = block || ->(_) {} #no-op + @block_to_get_arguments_to_run = block || ->(_) {} # no-op accept_test_for_nil = false - all_users.each do |k,v| + all_users.each do |k, v| os = OpenStruct.new os.key = k os.value = v if k.nil? - accept(user_to_signin: nil, method:@method, action: @action, args: @block_to_get_arguments_to_run.call(v)) + accept(user_to_signin: nil, method: @method, action: @action, args: @block_to_get_arguments_to_run.call(v)) accept_test_for_nil = true end if @successful_users.include? k - accept(user_to_signin: os, method:@method, action: @action, args: @block_to_get_arguments_to_run.call(v)) + accept(user_to_signin: os, method: @method, action: @action, args: @block_to_get_arguments_to_run.call(v)) else - reject(user_to_signin: os, method:@method, action: @action, args: @block_to_get_arguments_to_run.call(v)) + reject(user_to_signin: os, method: @method, action: @action, args: @block_to_get_arguments_to_run.call(v)) end end unless accept_test_for_nil - reject(user_to_signin: nil, method:@method, action: @action, args: @block_to_get_arguments_to_run.call(nil)) + reject(user_to_signin: nil, method: @method, action: @action, args: @block_to_get_arguments_to_run.call(nil)) end end - end - - diff --git a/spec/controllers/aws_presigned_posts_spec.rb b/spec/controllers/aws_presigned_posts_spec.rb index 51e71c04..d36a52f3 100644 --- a/spec/controllers/aws_presigned_posts_spec.rb +++ b/spec/controllers/aws_presigned_posts_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe AwsPresignedPostsController, :type => :controller do +describe AwsPresignedPostsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do @@ -11,4 +13,4 @@ describe AwsPresignedPostsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/billing_subscriptions_spec.rb b/spec/controllers/billing_subscriptions_spec.rb index 0ec55691..e5c99e13 100644 --- a/spec/controllers/billing_subscriptions_spec.rb +++ b/spec/controllers/billing_subscriptions_spec.rb @@ -1,24 +1,26 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe BillingSubscriptionsController, :type => :controller do +describe BillingSubscriptionsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'create_trial' do - include_context :open_to_np_admin, :post, :create_trial, nonprofit_id: :__our_np + include_context :open_to_np_admin, :post, :create_trial, nonprofit_id: :__our_np end describe 'create' do - include_context :open_to_np_admin, :post, :create, nonprofit_id: :__our_np + include_context :open_to_np_admin, :post, :create, nonprofit_id: :__our_np end describe 'cancel' do - include_context :open_to_np_admin, :post, :cancel, nonprofit_id: :__our_np + include_context :open_to_np_admin, :post, :cancel, nonprofit_id: :__our_np end describe 'cancellation' do - include_context :open_to_np_admin, :get, :cancellation, nonprofit_id: :__our_np + include_context :open_to_np_admin, :get, :cancellation, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaign_gift_options_spec.rb b/spec/controllers/campaign_gift_options_spec.rb index 84d20f4f..11bbbcb9 100644 --- a/spec/controllers/campaign_gift_options_spec.rb +++ b/spec/controllers/campaign_gift_options_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe CampaignGiftOptionsController, :type => :controller do +describe CampaignGiftOptionsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'reject unauthorized' do @@ -11,14 +13,14 @@ describe CampaignGiftOptionsController, :type => :controller do end describe 'update' do - include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" + include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: '1' end describe 'destroy' do - include_context :open_to_campaign_editor, :delete, :destroy, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" + include_context :open_to_campaign_editor, :delete, :destroy, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: '1' end describe 'update_order' do - include_context :open_to_campaign_editor, :put, :update_order, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" + include_context :open_to_campaign_editor, :put, :update_order, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: '1' end end @@ -28,8 +30,8 @@ describe CampaignGiftOptionsController, :type => :controller do end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: "1" + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, campaign_id: :__our_campaign, id: '1' end end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaign_gifts_spec.rb b/spec/controllers/campaign_gifts_spec.rb index 06f3f599..52356cd7 100644 --- a/spec/controllers/campaign_gifts_spec.rb +++ b/spec/controllers/campaign_gifts_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe CampaignGiftsController, :type => :controller do +describe CampaignGiftsController, type: :controller do describe 'authorization' do include_context :shared_user_context @@ -12,4 +14,4 @@ describe CampaignGiftsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaigns/campaign_gift_options_spec.rb b/spec/controllers/campaigns/campaign_gift_options_spec.rb index 58221ca4..3bbad02a 100644 --- a/spec/controllers/campaigns/campaign_gift_options_spec.rb +++ b/spec/controllers/campaigns/campaign_gift_options_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Campaigns::CampaignGiftOptionsController, :type => :controller do +describe Campaigns::CampaignGiftOptionsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'accept all' do describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign end end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaigns/donations_spec.rb b/spec/controllers/campaigns/donations_spec.rb index 11c40336..06be9a01 100644 --- a/spec/controllers/campaigns/donations_spec.rb +++ b/spec/controllers/campaigns/donations_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Campaigns::DonationsController, :type => :controller do +describe Campaigns::DonationsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'reject unauthorized' do describe 'index' do - include_context :open_to_campaign_editor, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign + include_context :open_to_campaign_editor, :get, :index, nonprofit_id: :__our_np, campaign_id: :__our_campaign end end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaigns/supporters_spec.rb b/spec/controllers/campaigns/supporters_spec.rb index fcaf2d22..100d280c 100644 --- a/spec/controllers/campaigns/supporters_spec.rb +++ b/spec/controllers/campaigns/supporters_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Campaigns::SupportersController, :type => :controller do +describe Campaigns::SupportersController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'reject unauthorized' do @@ -11,4 +13,4 @@ describe Campaigns::SupportersController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/campaigns_spec.rb b/spec/controllers/campaigns_spec.rb index 4673e8b0..b95770c5 100644 --- a/spec/controllers/campaigns_spec.rb +++ b/spec/controllers/campaigns_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe CampaignsController, :type => :controller do +describe CampaignsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do @@ -19,48 +21,47 @@ describe CampaignsController, :type => :controller do end describe 'update' do - include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_campaign_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_campaign end describe 'soft_delete' do - include_context :open_to_campaign_editor, :delete, :soft_delete, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_campaign_editor, :delete, :soft_delete, nonprofit_id: :__our_np, id: :__our_campaign end - end describe 'open to all' do describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_campaign end describe 'activities' do - include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_campaign end describe 'metrics' do - include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_campaign end describe 'timeline' do - include_context :open_to_all, :get, :timeline, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_all, :get, :timeline, nonprofit_id: :__our_np, id: :__our_campaign end describe 'totals' do - include_context :open_to_all, :get, :totals, nonprofit_id: :__our_np, id: :__our_campaign + include_context :open_to_all, :get, :totals, nonprofit_id: :__our_np, id: :__our_campaign end describe 'peer_to_peer' do - include_context :open_to_all, :get, :peer_to_peer, nonprofit_id: :__our_np + include_context :open_to_all, :get, :peer_to_peer, nonprofit_id: :__our_np end end end describe 'routes' do - it "routes campaigns#index" do - expect(get: "/nonprofits/5/campaigns/4").to(route_to(controller: "campaigns", action: "show", nonprofit_id: "5", id: "4")) + it 'routes campaigns#index' do + expect(get: '/nonprofits/5/campaigns/4').to(route_to(controller: 'campaigns', action: 'show', nonprofit_id: '5', id: '4')) end end end diff --git a/spec/controllers/cards_spec.rb b/spec/controllers/cards_spec.rb index 3aac440b..8fd67de8 100644 --- a/spec/controllers/cards_spec.rb +++ b/spec/controllers/cards_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe CardsController, :type => :controller do +describe CardsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'accept all' do describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end end end -end \ No newline at end of file +end diff --git a/spec/controllers/direct_debit_details_spec.rb b/spec/controllers/direct_debit_details_spec.rb index 44f29060..72c068d6 100644 --- a/spec/controllers/direct_debit_details_spec.rb +++ b/spec/controllers/direct_debit_details_spec.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe DirectDebitDetailsController, :type => :controller do +describe DirectDebitDetailsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'open to all' do describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end - - end end -end \ No newline at end of file +end diff --git a/spec/controllers/email_settings_spec.rb b/spec/controllers/email_settings_spec.rb index 9b7c49dd..dcc76e12 100644 --- a/spec/controllers/email_settings_spec.rb +++ b/spec/controllers/email_settings_spec.rb @@ -1,21 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe EmailSettingsController, :type => :controller do +describe EmailSettingsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do - include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np + include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np end describe 'index' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np end - end - - end -end \ No newline at end of file +end diff --git a/spec/controllers/emails_spec.rb b/spec/controllers/emails_spec.rb index 26d1eb40..cc0c3d00 100644 --- a/spec/controllers/emails_spec.rb +++ b/spec/controllers/emails_spec.rb @@ -1,16 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe EmailsController, :type => :controller do +describe EmailsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do include_context :open_to_registered, :post, :create end - - end end -end \ No newline at end of file +end diff --git a/spec/controllers/event_discounts_spec.rb b/spec/controllers/event_discounts_spec.rb index 4a723b8f..b3775bfc 100644 --- a/spec/controllers/event_discounts_spec.rb +++ b/spec/controllers/event_discounts_spec.rb @@ -1,30 +1,30 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe EventDiscountsController, :type => :controller do +describe EventDiscountsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do - include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event end - + describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end - + describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end - - end - + describe 'open to all' do describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event, id: "2" + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end end end -end \ No newline at end of file +end diff --git a/spec/controllers/events_spec.rb b/spec/controllers/events_spec.rb index 5d832eb6..7ae705c8 100644 --- a/spec/controllers/events_spec.rb +++ b/spec/controllers/events_spec.rb @@ -1,53 +1,51 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe EventsController, :type => :controller do +describe EventsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'create' do - include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, id: :__our_event end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, id: :__our_event end describe 'duplicate' do - include_context :open_to_event_editor, :post, :duplicate, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_event_editor, :post, :duplicate, nonprofit_id: :__our_np, id: :__our_event end describe 'soft_delete' do - include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :delete, :soft_delete, nonprofit_id: :__our_np, event_id: :__our_event end describe 'stats' do - include_context :open_to_event_editor, :get, :stats, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_event_editor, :get, :stats, nonprofit_id: :__our_np, id: :__our_event end describe 'name_and_id' do - include_context :open_to_np_associate, :get, :name_and_id, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :name_and_id, nonprofit_id: :__our_np end end describe 'open to all' do describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np end describe 'listings' do - include_context :open_to_all, :get, :listings, nonprofit_id: :__our_np + include_context :open_to_all, :get, :listings, nonprofit_id: :__our_np end describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, id: :__our_event end describe 'activities' do - include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_all, :get, :activities, nonprofit_id: :__our_np, id: :__our_event end describe 'metrics' do - include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_event + include_context :open_to_all, :get, :metrics, nonprofit_id: :__our_np, id: :__our_event end - - - end - -end \ No newline at end of file +end diff --git a/spec/controllers/front_spec.rb b/spec/controllers/front_spec.rb index b64291c6..0161360b 100644 --- a/spec/controllers/front_spec.rb +++ b/spec/controllers/front_spec.rb @@ -1,19 +1,21 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe FrontController, :type => :controller do +describe FrontController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'accept all' do describe 'index' do - include_context :open_to_all, :get, :index + include_context :open_to_all, :get, :index end end end it 'index redirects to onboard with no non-profits' do - get( :index) + get(:index) expect(response).to redirect_to onboard_url end @@ -37,6 +39,5 @@ describe FrontController, :type => :controller do get(:index) expect(response).to redirect_to profile_url(unauth_user.profile.id) end - end -end \ No newline at end of file +end diff --git a/spec/controllers/image_attachments_spec.rb b/spec/controllers/image_attachments_spec.rb index c38ec151..cfd6dab1 100644 --- a/spec/controllers/image_attachments_spec.rb +++ b/spec/controllers/image_attachments_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe ImageAttachmentsController, :type => :controller do +describe ImageAttachmentsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do @@ -15,4 +17,4 @@ describe ImageAttachmentsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/maps_spec.rb b/spec/controllers/maps_spec.rb index 6fcebd00..b70595f8 100644 --- a/spec/controllers/maps_spec.rb +++ b/spec/controllers/maps_spec.rb @@ -1,30 +1,30 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe MapsController, :type => :controller do +describe MapsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'all_supporters' do - include_context :open_to_super_admin, :get, :all_supporters + include_context :open_to_super_admin, :get, :all_supporters end describe 'all_npo_supporters' do - include_context :open_to_np_associate, :get, :all_npo_supporters, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :all_npo_supporters, nonprofit_id: :__our_np end describe 'specific_npo_supporters' do - include_context :open_to_np_associate, :get, :specific_npo_supporters, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :specific_npo_supporters, nonprofit_id: :__our_np end end describe 'open_to_all' do describe 'all_npos' do - include_context :open_to_all, :get, :all_npos, nonprofit_id: :__our_np + include_context :open_to_all, :get, :all_npos, nonprofit_id: :__our_np end - - end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/activities_spec.rb b/spec/controllers/nonprofits/activities_spec.rb index ec3dd383..dd284891 100644 --- a/spec/controllers/nonprofits/activities_spec.rb +++ b/spec/controllers/nonprofits/activities_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::ActivitiesController, :type => :controller do +describe Nonprofits::ActivitiesController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do @@ -11,4 +13,4 @@ describe Nonprofits::ActivitiesController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/bank_accounts_spec.rb b/spec/controllers/nonprofits/bank_accounts_spec.rb index a9a9c10b..f58bc0ca 100644 --- a/spec/controllers/nonprofits/bank_accounts_spec.rb +++ b/spec/controllers/nonprofits/bank_accounts_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::BankAccountsController, :type => :controller do +describe Nonprofits::BankAccountsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'create' do @@ -29,4 +31,4 @@ describe Nonprofits::BankAccountsController, :type => :controller do include_context :open_to_np_admin, :post, :resend_confirmation, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/button_spec.rb b/spec/controllers/nonprofits/button_spec.rb index cd60b48e..683d2e8e 100644 --- a/spec/controllers/nonprofits/button_spec.rb +++ b/spec/controllers/nonprofits/button_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::ButtonController, :type => :controller do +describe Nonprofits::ButtonController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'send_code' do @@ -20,7 +22,5 @@ describe Nonprofits::ButtonController, :type => :controller do describe 'advanced' do include_context :open_to_registered, :get, :advanced, nonprofit_id: :__our_np end - - end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/cards_spec.rb b/spec/controllers/nonprofits/cards_spec.rb index bf539727..f0568692 100644 --- a/spec/controllers/nonprofits/cards_spec.rb +++ b/spec/controllers/nonprofits/cards_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::CardsController, :type => :controller do +describe Nonprofits::CardsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'show' do @@ -13,4 +15,4 @@ describe Nonprofits::CardsController, :type => :controller do include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/charges_spec.rb b/spec/controllers/nonprofits/charges_spec.rb index 0bc2f4fd..d6325ed5 100644 --- a/spec/controllers/nonprofits/charges_spec.rb +++ b/spec/controllers/nonprofits/charges_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::ChargesController, :type => :controller do +describe Nonprofits::ChargesController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'get' do include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/custom_field_masters_spec.rb b/spec/controllers/nonprofits/custom_field_masters_spec.rb index a7bf7953..318f4146 100644 --- a/spec/controllers/nonprofits/custom_field_masters_spec.rb +++ b/spec/controllers/nonprofits/custom_field_masters_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::CustomFieldMastersController, :type => :controller do +describe Nonprofits::CustomFieldMastersController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'get payments' do @@ -17,4 +19,4 @@ describe Nonprofits::CustomFieldMastersController, :type => :controller do include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/custom_fields_joins_spec.rb b/spec/controllers/nonprofits/custom_fields_joins_spec.rb index ab3a4e2d..b58f1acf 100644 --- a/spec/controllers/nonprofits/custom_fields_joins_spec.rb +++ b/spec/controllers/nonprofits/custom_fields_joins_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::CustomFieldJoinsController, :type => :controller do +describe Nonprofits::CustomFieldJoinsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'index' do @@ -10,11 +12,11 @@ describe Nonprofits::CustomFieldJoinsController, :type => :controller do end describe 'modify' do - include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: "1" + include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: '1' end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: "1", supporter_id: 1 + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1', supporter_id: 1 end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/donations_spec.rb b/spec/controllers/nonprofits/donations_spec.rb index 79614910..f9c85070 100644 --- a/spec/controllers/nonprofits/donations_spec.rb +++ b/spec/controllers/nonprofits/donations_spec.rb @@ -1,25 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' require 'controllers/support/new_controller_user_context' require 'support/contexts/shared_donation_charge_context' -describe Nonprofits::DonationsController, :type => :controller do - +describe Nonprofits::DonationsController, type: :controller do describe 'rejects unauthenticated users' do describe 'index' do include_context :shared_user_context - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, id: "1" + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, id: '1' end - - describe 'update' do include_context :shared_user_context - include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: "1" + include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: '1' end - - end describe 'accept all users' do describe 'create' do @@ -27,12 +24,12 @@ describe Nonprofits::DonationsController, :type => :controller do end describe 'follow up' do - include_context :open_to_all, :put, :followup, nonprofit_id: :__our_np, id: "1" + include_context :open_to_all, :put, :followup, nonprofit_id: :__our_np, id: '1' end end end -describe '.create_offsite', :type => :request do +describe '.create_offsite', type: :request do describe 'create_offsite' do include_context :shared_donation_charge_context include_context :general_shared_user_context @@ -45,6 +42,6 @@ describe '.create_offsite', :type => :request do # donation: {campaign_id: campaign.id}} # end # end - #include_context :open_to_np_associate, :post, :create_offsite, nonprofit_id: :__our_np + # include_context :open_to_np_associate, :post, :create_offsite, nonprofit_id: :__our_np end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/email_lists_spec.rb b/spec/controllers/nonprofits/email_lists_spec.rb index 737f3e62..6c094e2e 100644 --- a/spec/controllers/nonprofits/email_lists_spec.rb +++ b/spec/controllers/nonprofits/email_lists_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::EmailListsController, :type => :controller do +describe Nonprofits::EmailListsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'index' do @@ -13,4 +15,4 @@ describe Nonprofits::EmailListsController, :type => :controller do include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/imports_spec.rb b/spec/controllers/nonprofits/imports_spec.rb index 93f2f833..ac599957 100644 --- a/spec/controllers/nonprofits/imports_spec.rb +++ b/spec/controllers/nonprofits/imports_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::ImportsController, :type => :controller do +describe Nonprofits::ImportsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'create' do include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/miscellaneous_np_infos_spec.rb b/spec/controllers/nonprofits/miscellaneous_np_infos_spec.rb index 2c6f7720..7fb3d53d 100644 --- a/spec/controllers/nonprofits/miscellaneous_np_infos_spec.rb +++ b/spec/controllers/nonprofits/miscellaneous_np_infos_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::MiscellaneousNpInfosController, :type => :controller do +describe Nonprofits::MiscellaneousNpInfosController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'show' do @@ -13,4 +15,4 @@ describe Nonprofits::MiscellaneousNpInfosController, :type => :controller do include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/nonprofit_keys_spec.rb b/spec/controllers/nonprofits/nonprofit_keys_spec.rb index 13ed8c12..0832490d 100644 --- a/spec/controllers/nonprofits/nonprofit_keys_spec.rb +++ b/spec/controllers/nonprofits/nonprofit_keys_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::NonprofitKeysController, :type => :controller do +describe Nonprofits::NonprofitKeysController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'index' do @@ -17,4 +19,4 @@ describe Nonprofits::NonprofitKeysController, :type => :controller do include_context :open_to_np_associate, :get, :mailchimp_landing, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/payments_spec.rb b/spec/controllers/nonprofits/payments_spec.rb index 7750b6df..7c533242 100644 --- a/spec/controllers/nonprofits/payments_spec.rb +++ b/spec/controllers/nonprofits/payments_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::PaymentsController, :type => :controller do +describe Nonprofits::PaymentsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'get payments' do @@ -25,4 +27,4 @@ describe Nonprofits::PaymentsController, :type => :controller do include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/payouts_spec.rb b/spec/controllers/nonprofits/payouts_spec.rb index 82a5fc96..8060d17c 100644 --- a/spec/controllers/nonprofits/payouts_spec.rb +++ b/spec/controllers/nonprofits/payouts_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::PayoutsController, :type => :controller do +describe Nonprofits::PayoutsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'create' do @@ -16,7 +18,5 @@ describe Nonprofits::PayoutsController, :type => :controller do describe 'show' do include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1' end - - end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/recurring_donations_spec.rb b/spec/controllers/nonprofits/recurring_donations_spec.rb index 4ad14e70..e3de65a8 100644 --- a/spec/controllers/nonprofits/recurring_donations_spec.rb +++ b/spec/controllers/nonprofits/recurring_donations_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::RecurringDonationsController, :type => :controller do +describe Nonprofits::RecurringDonationsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'index' do @@ -24,8 +26,6 @@ describe Nonprofits::RecurringDonationsController, :type => :controller do describe 'update' do include_context :open_to_np_associate, :put, :update, nonprofit_id: :__our_np, id: '1' end - - end describe 'open for all' do @@ -33,4 +33,4 @@ describe Nonprofits::RecurringDonationsController, :type => :controller do include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/reports_spec.rb b/spec/controllers/nonprofits/reports_spec.rb index 203d51e2..d401b78f 100644 --- a/spec/controllers/nonprofits/reports_spec.rb +++ b/spec/controllers/nonprofits/reports_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::ReportsController, :type => :controller do +describe Nonprofits::ReportsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'end_of_year' do @@ -13,4 +15,4 @@ describe Nonprofits::ReportsController, :type => :controller do include_context :open_to_np_associate, :get, :end_of_year_custom, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/supporter_emails_spec.rb b/spec/controllers/nonprofits/supporter_emails_spec.rb index 66410728..681aa5f2 100644 --- a/spec/controllers/nonprofits/supporter_emails_spec.rb +++ b/spec/controllers/nonprofits/supporter_emails_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::SupporterEmailsController, :type => :controller do +describe Nonprofits::SupporterEmailsController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'create' do @@ -13,4 +15,4 @@ describe Nonprofits::SupporterEmailsController, :type => :controller do include_context :open_to_np_associate, :post, :gmail, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/supporters_spec.rb b/spec/controllers/nonprofits/supporters_spec.rb index ac544e24..70cd8bfa 100644 --- a/spec/controllers/nonprofits/supporters_spec.rb +++ b/spec/controllers/nonprofits/supporters_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::SupportersController, :type => :controller do +describe Nonprofits::SupportersController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do describe 'index' do @@ -47,4 +49,4 @@ describe Nonprofits::SupportersController, :type => :controller do include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/tag_joins_spec.rb b/spec/controllers/nonprofits/tag_joins_spec.rb index a9ff6327..70138d13 100644 --- a/spec/controllers/nonprofits/tag_joins_spec.rb +++ b/spec/controllers/nonprofits/tag_joins_spec.rb @@ -1,22 +1,22 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::TagJoinsController, :type => :controller do +describe Nonprofits::TagJoinsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'index' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, supporter_id: 1 + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, supporter_id: 1 end - + describe 'modify' do - include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: '1' + include_context :open_to_np_associate, :post, :modify, nonprofit_id: :__our_np, id: '1' end - + describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1', supporter_id: 2 + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1', supporter_id: 2 end - - end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/tag_masters_spec.rb b/spec/controllers/nonprofits/tag_masters_spec.rb index 999f4072..7239c435 100644 --- a/spec/controllers/nonprofits/tag_masters_spec.rb +++ b/spec/controllers/nonprofits/tag_masters_spec.rb @@ -1,22 +1,24 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::TagMastersController, :type => :controller do +describe Nonprofits::TagMastersController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'index' do - include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np + include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np end describe 'create' do - include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np + include_context :open_to_np_associate, :post, :create, nonprofit_id: :__our_np end describe 'destroy' do - include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' + include_context :open_to_np_associate, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits/trackings_spec.rb b/spec/controllers/nonprofits/trackings_spec.rb index fd27c4f9..4ec0311c 100644 --- a/spec/controllers/nonprofits/trackings_spec.rb +++ b/spec/controllers/nonprofits/trackings_spec.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe Nonprofits::TrackingsController, :type => :controller do +describe Nonprofits::TrackingsController, type: :controller do include_context :shared_user_context describe 'open to all' do describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end end -end \ No newline at end of file +end diff --git a/spec/controllers/nonprofits_spec.rb b/spec/controllers/nonprofits_spec.rb index 67b6fe6f..8b74c98a 100644 --- a/spec/controllers/nonprofits_spec.rb +++ b/spec/controllers/nonprofits_spec.rb @@ -1,73 +1,73 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe NonprofitsController, :type => :controller do +describe NonprofitsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'update' do - include_context :open_to_np_associate, :put, :update, id: :__our_np + include_context :open_to_np_associate, :put, :update, id: :__our_np end describe 'dashboard' do - include_context :open_to_np_associate, :get, :dashboard, id: :__our_np + include_context :open_to_np_associate, :get, :dashboard, id: :__our_np end describe 'dashboard_metrics' do - include_context :open_to_np_associate, :get, :dashboard_metrics, id: :__our_np + include_context :open_to_np_associate, :get, :dashboard_metrics, id: :__our_np end describe 'verify_identity' do - include_context :open_to_np_associate, :put, :verify_identity, id: :__our_np + include_context :open_to_np_associate, :put, :verify_identity, id: :__our_np end describe 'recurring_donation_stats' do - include_context :open_to_np_associate, :get, :recurring_donation_stats, id: :__our_np + include_context :open_to_np_associate, :get, :recurring_donation_stats, id: :__our_np end describe 'profile_todos' do - include_context :open_to_np_associate, :get, :profile_todos, id: :__our_np + include_context :open_to_np_associate, :get, :profile_todos, id: :__our_np end describe 'dashboard_todos' do - include_context :open_to_np_associate, :get, :dashboard_todos, id: :__our_np + include_context :open_to_np_associate, :get, :dashboard_todos, id: :__our_np end describe 'payment_history' do - include_context :open_to_np_associate, :get, :payment_history, id: :__our_np + include_context :open_to_np_associate, :get, :payment_history, id: :__our_np end - - describe 'destroy' do - include_context :open_to_super_admin, :delete, :destroy, id: :__our_np + include_context :open_to_super_admin, :delete, :destroy, id: :__our_np end end describe 'open to all' do describe 'show' do - include_context :open_to_all, :get, :show, id: :__our_np + include_context :open_to_all, :get, :show, id: :__our_np end describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np end describe 'btn' do - include_context :open_to_all, :get, :btn, id: :__our_np + include_context :open_to_all, :get, :btn, id: :__our_np end describe 'supporter_form' do - include_context :open_to_all, :get, :supporter_form, id: :__our_np + include_context :open_to_all, :get, :supporter_form, id: :__our_np end describe 'custom_supporter' do - include_context :open_to_all, :post, :custom_supporter, id: :__our_np + include_context :open_to_all, :post, :custom_supporter, id: :__our_np end describe 'donate' do - include_context :open_to_all, :get, :donate, id: :__our_np + include_context :open_to_all, :get, :donate, id: :__our_np end describe 'search' do @@ -75,4 +75,4 @@ describe NonprofitsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/onboard_controller_spec.rb b/spec/controllers/onboard_controller_spec.rb index 0471da11..7e7b0dcc 100644 --- a/spec/controllers/onboard_controller_spec.rb +++ b/spec/controllers/onboard_controller_spec.rb @@ -1,5 +1,6 @@ +# frozen_string_literal: true + require 'rails_helper' -RSpec.describe OnboardController, :type => :controller do - +RSpec.describe OnboardController, type: :controller do end diff --git a/spec/controllers/profiles_spec.rb b/spec/controllers/profiles_spec.rb index 764f08b5..e777f38c 100644 --- a/spec/controllers/profiles_spec.rb +++ b/spec/controllers/profiles_spec.rb @@ -1,21 +1,23 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe ProfilesController, :type => :controller do +describe ProfilesController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'update' do - include_context :open_to_profile_owner, :put, :update, id: :__our_profile + include_context :open_to_profile_owner, :put, :update, id: :__our_profile end describe 'fundraisers' do - include_context :open_to_profile_owner, :get, :fundraisers, id: :__our_profile + include_context :open_to_profile_owner, :get, :fundraisers, id: :__our_profile end describe 'donations_history' do - include_context :open_to_profile_owner, :get, :donations_history, id: :__our_profile + include_context :open_to_profile_owner, :get, :donations_history, id: :__our_profile end end @@ -25,4 +27,4 @@ describe ProfilesController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/recurring_donations_spec.rb b/spec/controllers/recurring_donations_spec.rb index 122a04cc..d6d8e966 100644 --- a/spec/controllers/recurring_donations_spec.rb +++ b/spec/controllers/recurring_donations_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe RecurringDonationsController, :type => :controller do +describe RecurringDonationsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'open to all (note: edit token is checked inside methods)' do @@ -23,4 +25,4 @@ describe RecurringDonationsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/roles_spec.rb b/spec/controllers/roles_spec.rb index 6b746d62..364a17c7 100644 --- a/spec/controllers/roles_spec.rb +++ b/spec/controllers/roles_spec.rb @@ -1,20 +1,20 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe RolesController, :type => :controller do +describe RolesController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do - include_context :open_to_np_admin, :post, :create, nonprofit_id: :__our_np + include_context :open_to_np_admin, :post, :create, nonprofit_id: :__our_np end - + describe 'destroy' do - include_context :open_to_np_admin, :delete, :destroy, nonprofit_id: :__our_np, id: '1' + include_context :open_to_np_admin, :delete, :destroy, nonprofit_id: :__our_np, id: '1' end - - end end -end \ No newline at end of file +end diff --git a/spec/controllers/settings_spec.rb b/spec/controllers/settings_spec.rb index 31ac7c28..61f42021 100644 --- a/spec/controllers/settings_spec.rb +++ b/spec/controllers/settings_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe SettingsController, :type => :controller do +describe SettingsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do @@ -11,4 +13,4 @@ describe SettingsController, :type => :controller do end end end -end \ No newline at end of file +end diff --git a/spec/controllers/static_controller_spec.rb b/spec/controllers/static_controller_spec.rb index dc1a1a88..3c1ffc22 100644 --- a/spec/controllers/static_controller_spec.rb +++ b/spec/controllers/static_controller_spec.rb @@ -1,8 +1,10 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' -RSpec.describe StaticController, :type => :controller do - describe ".ccs" do +RSpec.describe StaticController, type: :controller do + describe '.ccs' do around(:each) do |example| example.run Settings.reload! @@ -11,36 +13,30 @@ RSpec.describe StaticController, :type => :controller do describe 'local_tar_gz' do before (:each) do Settings.merge!( - { - ccs: { - ccs_method: 'local_tar_gz', - } - }) + ccs: { + ccs_method: 'local_tar_gz' + } + ) end - it 'fails on git archive' do expect(Kernel).to receive(:system).and_return(false) get('ccs') expect(response.status).to eq 500 end - end it 'setup github' do - Settings.merge!( - { - ccs: { - ccs_method: 'github', - options: { - account: 'account', - repo: 'repo' - } - } - }) + Settings[:ccs] = { + ccs_method: 'github', + options: { + account: 'account', + repo: 'repo' + } + } expect(File).to receive(:read).with("#{Rails.root}/CCS_HASH").and_return("hash\n") get('ccs') - expect(response).to redirect_to "https://github.com/account/repo/tree/hash" + expect(response).to redirect_to 'https://github.com/account/repo/tree/hash' end end end diff --git a/spec/controllers/super_admins_spec.rb b/spec/controllers/super_admins_spec.rb index af6e5c46..418d551b 100644 --- a/spec/controllers/super_admins_spec.rb +++ b/spec/controllers/super_admins_spec.rb @@ -1,28 +1,28 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe SuperAdminsController, :type => :controller do +describe SuperAdminsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'search_nonprofits' do - include_context :open_to_super_admin, :get, :search_nonprofits + include_context :open_to_super_admin, :get, :search_nonprofits end describe 'search_profiles' do - include_context :open_to_super_admin, :get, :search_profiles + include_context :open_to_super_admin, :get, :search_profiles end describe 'search_fullcontact' do - include_context :open_to_super_admin, :get, :search_fullcontact + include_context :open_to_super_admin, :get, :search_fullcontact end describe 'index' do - include_context :open_to_super_admin, :get, :index + include_context :open_to_super_admin, :get, :index end - - end end -end \ No newline at end of file +end diff --git a/spec/controllers/support/new_controller_user_context.rb b/spec/controllers/support/new_controller_user_context.rb index 7c08bf81..37fb793d 100644 --- a/spec/controllers/support/new_controller_user_context.rb +++ b/spec/controllers/support/new_controller_user_context.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'support/contexts/general_shared_user_context' @@ -5,7 +7,7 @@ RSpec.shared_context :new_controller_user_context do include_context :general_shared_user_context def sign_in(user_to_signin) - post_via_redirect 'users/sign_in', 'user[email]' => user_to_signin.email, 'user[password]' => user_to_signin.password, format: "json" + post_via_redirect 'users/sign_in', 'user[email]' => user_to_signin.email, 'user[password]' => user_to_signin.password, format: 'json' end def sign_out @@ -15,19 +17,19 @@ RSpec.shared_context :new_controller_user_context do def send(method, *args) case method when :get - return xhr(:get, *args) + xhr(:get, *args) when :post - return xhr(:post, *args) + xhr(:post, *args) when :delete - return xhr(:delete, *args) + xhr(:delete, *args) when :put - return xhr(:put, *args) + xhr(:put, *args) end end def accept(user_to_signin:, method:, action:, args:) new_user = user_to_signin - if (user_to_signin != nil && user_to_signin.is_a?(OpenStruct)) + if !user_to_signin.nil? && user_to_signin.is_a?(OpenStruct) new_user = user_to_signin.value end sign_in new_user if new_user @@ -35,19 +37,18 @@ RSpec.shared_context :new_controller_user_context do # expect_any_instance_of(described_class).to receive(action).and_return(ActionController::TestResponse.new(200)) # expect_any_instance_of(described_class).to receive(:render).and_return(nil) send(method, action, args) - expect(response.status).to_not eq(302), "expected success for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ":" : "")} #{new_user&.attributes}" + expect(response.status).to_not eq(302), "expected success for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ':' : '')} #{new_user&.attributes}" sign_out end def reject(user_to_signin:, method:, action:, args:) - new_user = user_to_signin - if (user_to_signin != nil && user_to_signin.is_a?(OpenStruct)) + if !user_to_signin.nil? && user_to_signin.is_a?(OpenStruct) new_user = user_to_signin.value end sign_in new_user if new_user send(method, action, args) - expect(response.status).to eq(302), "expected failure for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ":" : "")} #{new_user&.attributes}" + expect(response.status).to eq(302), "expected failure for user: #{(user_to_signin.is_a?(OpenStruct) ? user_to_signin.key.to_s + ':' : '')} #{new_user&.attributes}" sign_out end -end \ No newline at end of file +end diff --git a/spec/controllers/support/shared_user_context.rb b/spec/controllers/support/shared_user_context.rb index 3abe31f6..503c98e6 100644 --- a/spec/controllers/support/shared_user_context.rb +++ b/spec/controllers/support/shared_user_context.rb @@ -1,97 +1,92 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later - RSpec.shared_context :shared_user_context do + let(:nonprofit) { force_create(:nonprofit, published: true) } + let(:other_nonprofit) { force_create(:nonprofit) } - - let(:nonprofit) {force_create(:nonprofit, published:true)} - let(:other_nonprofit) { force_create(:nonprofit)} - - - let(:user_as_np_admin) { + let(:user_as_np_admin) do __create_admin(nonprofit) - } + end - - let(:user_as_other_np_admin) { + let(:user_as_other_np_admin) do __create_admin(other_nonprofit) - } + end - let(:user_as_np_associate){ + let(:user_as_np_associate) do __create_associate(nonprofit) - } + end - let(:user_as_other_np_associate){ + let(:user_as_other_np_associate) do __create_associate(other_nonprofit) - } + end - let(:unauth_user) { + let(:unauth_user) do force_create(:user) - } + end - let(:campaign) {force_create(:campaign, nonprofit: nonprofit)} - let(:campaign_editor) { + let(:campaign) { force_create(:campaign, nonprofit: nonprofit) } + let(:campaign_editor) do __create(:campaign_editor, campaign) - } + end - let(:confirmed_user){ + let(:confirmed_user) do force_create(:user, confirmed_at: Time.current) - } + end - let(:event) { + let(:event) do force_create(:event, nonprofit: nonprofit) - } + end - let(:event_editor) { - __create(:event_editor,event) - } + let(:event_editor) do + __create(:event_editor, event) + end - let(:super_admin) { - __create(:super_admin, other_nonprofit) - } + let(:super_admin) do + __create(:super_admin, other_nonprofit) + end - let(:user_with_profile) { + let(:user_with_profile) do u = force_create(:user) force_create(:profile, user: u) u - } - - + end def __create(name, host) u = force_create(:user) - force_create(:role, user: u, name: name, host:host) + force_create(:role, user: u, name: name, host: host) u end def __create_admin(host) u = force_create(:user) - force_create(:role, user: u, name: :nonprofit_admin, host:host) + force_create(:role, user: u, name: :nonprofit_admin, host: host) u end def __create_associate(host) u = force_create(:user) - force_create(:role, user: u, name: :nonprofit_associate, host:host) + force_create(:role, user: u, name: :nonprofit_associate, host: host) u end def send(method, *args) case method - when :get - return get(*args) - when :post - return post(*args) - when :delete - return delete(*args) - when :put - return put(*args) + when :get + get(*args) + when :post + post(*args) + when :delete + delete(*args) + when :put + put(*args) end end def accept(user_to_signin, method, action, *args) - without_json_response = [:cancellation, :all_npos].include?(action) - request.accept = "application/json" unless without_json_response + without_json_response = %i[cancellation all_npos confirmation peer_to_peer].include?(action) + request.accept = 'application/json' unless without_json_response sign_in user_to_signin if user_to_signin # allows us to run the helpers but ignore what the controller action does @@ -121,42 +116,38 @@ RSpec.shared_context :shared_user_context do def fix_args(*args) replacements = { - __our_np: nonprofit.id, - __our_campaign: campaign.id, - __our_event: event.id, - __our_profile: user_with_profile.profile.id + __our_np: nonprofit.id, + __our_campaign: campaign.id, + __our_event: event.id, + __our_profile: user_with_profile.profile.id } - args.collect{|i| + args.collect do |i| ret = i if replacements[i] ret = replacements[i] elsif i.is_a? Hash - ret = i.collect{|k,v | + ret = i.collect do |k, v| ret_v = v - if replacements[v] - ret_v = replacements[v] - end + ret_v = replacements[v] if replacements[v] - [k,ret_v] - }.to_h + [k, ret_v] + end.to_h end ret - }.to_a + end.to_a end - - end RSpec.shared_context :open_to_all do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'accepts no user' do accept(nil, method, action, *fixed_args) end @@ -200,14 +191,13 @@ RSpec.shared_context :open_to_all do |method, action, *args| it 'accept profile user' do accept(user_with_profile, method, action, *fixed_args) end - end RSpec.shared_context :open_to_np_associate do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ + let(:fixed_args) do fix_args(*args) - } + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -254,12 +244,11 @@ RSpec.shared_context :open_to_np_associate do |method, action, *args| end end - RSpec.shared_context :open_to_np_admin do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -307,9 +296,9 @@ end RSpec.shared_context :open_to_registered do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -355,12 +344,11 @@ RSpec.shared_context :open_to_registered do |method, action, *args| end end - RSpec.shared_context :open_to_campaign_editor do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -404,14 +392,13 @@ RSpec.shared_context :open_to_campaign_editor do |method, action, *args| it 'rejects profile user' do reject(user_with_profile, method, action, *fixed_args) end - end RSpec.shared_context :open_to_confirmed_users do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -455,14 +442,13 @@ RSpec.shared_context :open_to_confirmed_users do |method, action, *args| it 'rejects profile user' do reject(user_with_profile, method, action, *fixed_args) end - end RSpec.shared_context :open_to_event_editor do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -509,9 +495,9 @@ end RSpec.shared_context :open_to_super_admin do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) @@ -558,12 +544,11 @@ RSpec.shared_context :open_to_super_admin do |method, action, *args| end end - RSpec.shared_context :open_to_profile_owner do |method, action, *args| include_context :shared_user_context - let(:fixed_args){ - fix_args( *args) - } + let(:fixed_args) do + fix_args(*args) + end it 'rejects no user' do reject(nil, method, action, *fixed_args) diff --git a/spec/controllers/ticket_levels_spec.rb b/spec/controllers/ticket_levels_spec.rb index d9b5cd6f..7fb63bd9 100644 --- a/spec/controllers/ticket_levels_spec.rb +++ b/spec/controllers/ticket_levels_spec.rb @@ -1,34 +1,35 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe TicketLevelsController, :type => :controller do +describe TicketLevelsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do describe 'create' do - include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" + include_context :open_to_event_editor, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event, id: '1' end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: '1' end describe 'update_order' do - include_context :open_to_event_editor, :put, :update_order, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_event_editor, :put, :update_order, nonprofit_id: :__our_np, event_id: :__our_event end describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: "1" + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: '1' end - end describe 'open to all' do describe 'show' do - include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' + include_context :open_to_all, :get, :show, nonprofit_id: :__our_np, event_id: :__our_event, id: '2' end describe 'index' do - include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_all, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event end end end -end \ No newline at end of file +end diff --git a/spec/controllers/tickets_spec.rb b/spec/controllers/tickets_spec.rb index 23faaa04..f8121d81 100644 --- a/spec/controllers/tickets_spec.rb +++ b/spec/controllers/tickets_spec.rb @@ -1,38 +1,38 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' require 'controllers/support/shared_user_context' -describe TicketsController, :type => :controller do +describe TicketsController, type: :controller do describe 'authorization' do include_context :shared_user_context describe 'rejects unauthorized users' do - describe 'index' do include_context :open_to_event_editor, :get, :index, nonprofit_id: :__our_np, event_id: :__our_event end describe 'update' do - include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 + include_context :open_to_event_editor, :put, :update, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 end describe 'destroy' do - include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 + include_context :open_to_event_editor, :delete, :destroy, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 end describe 'delete_card_for_ticket' do - include_context :open_to_np_associate, :post, :delete_card_for_ticket, nonprofit_id: :__our_np, event_id: :__our_event, id: 11111 + include_context :open_to_np_associate, :post, :delete_card_for_ticket, nonprofit_id: :__our_np, event_id: :__our_event, id: 11_111 end end describe 'open to all' do describe 'create' do - include_context :open_to_all, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event + include_context :open_to_all, :post, :create, nonprofit_id: :__our_np, event_id: :__our_event end describe 'add_note' do include_context :open_to_all, :put, :add_note, nonprofit_id: :__our_np, event_id: :__our_event, id: 1111 end - end end -end \ No newline at end of file +end diff --git a/spec/cve/cve_2014_2538_spec.rb b/spec/cve/cve_2014_2538_spec.rb index 53fcb21e..bc2b49ed 100644 --- a/spec/cve/cve_2014_2538_spec.rb +++ b/spec/cve/cve_2014_2538_spec.rb @@ -1,14 +1,16 @@ +# frozen_string_literal: true + # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'rails_helper' -require "rack/ssl" +require 'rack/ssl' describe Rack::SSL do describe '.call' do - it "invalid uri returns 404" do + it 'invalid uri returns 404' do def test_invalid_uri_returns_404 # Can't test this with Rack::Test because it fails on the URI before it # even gets to Rack::SSL. Other webservers will pass this URI through. ssl = Rack::SSL.new(nil) - resp = ssl.call('PATH_INFO' => "https://example.org/path/ -<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %> -
diff --git a/app/views/nonprofits/button/guided.html.erb b/app/views/nonprofits/button/guided.html.erb index 90d1f20e..4559ed40 100644 --- a/app/views/nonprofits/button/guided.html.erb +++ b/app/views/nonprofits/button/guided.html.erb @@ -8,8 +8,6 @@ <%= IncludeAsset.js '/client/js/nonprofits/button/page.js' %> <% end %> -<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %> -
diff --git a/app/views/nonprofits/cards/edit.html.erb b/app/views/nonprofits/cards/edit.html.erb index 57f16952..0969be8a 100644 --- a/app/views/nonprofits/cards/edit.html.erb +++ b/app/views/nonprofits/cards/edit.html.erb @@ -6,7 +6,6 @@ card: <%= @nonprofit.active_card ? raw(@nonprofit.active_card.to_json) : '{}' %> , plan: <%= raw(@nonprofit.billing_plan.to_json) %> , subscription: <%= raw(@nonprofit.billing_subscription.to_json) %> - , daysLeft : <%= QueryBillingSubscriptions.days_left_in_trial(@nonprofit.id) %> } <%= IncludeAsset.js '/client/js/nonprofits/cards/edit/page.js' %> diff --git a/app/views/nonprofits/dashboard.html.erb b/app/views/nonprofits/dashboard.html.erb index 3d9fdf84..9073e191 100644 --- a/app/views/nonprofits/dashboard.html.erb +++ b/app/views/nonprofits/dashboard.html.erb @@ -10,8 +10,6 @@ <%= IncludeAsset.js '/client/js/nonprofits/dashboard/page.js' %> <% end %> -<%= render '/components/trial_bar' if nonprofit_in_trial? %> - <%= render 'components/header', icon_class: 'icon-camera-graph-2', title: 'Dashboard', diff --git a/app/views/nonprofits/payments/index.html.erb b/app/views/nonprofits/payments/index.html.erb index 3611db16..d9ea327f 100644 --- a/app/views/nonprofits/payments/index.html.erb +++ b/app/views/nonprofits/payments/index.html.erb @@ -42,8 +42,6 @@ <% end %> -<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %> - <%= render 'nonprofits/transaction_title', active: :payments, icon_class: 'icon-piggy-bank', diff --git a/app/views/nonprofits/payouts/index.html.erb b/app/views/nonprofits/payouts/index.html.erb index ce62150a..fe528bfa 100644 --- a/app/views/nonprofits/payouts/index.html.erb +++ b/app/views/nonprofits/payouts/index.html.erb @@ -11,8 +11,6 @@ <% end %> -<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %> - <%= render 'nonprofits/transaction_title', active: :payouts, icon_class: 'icon-bank-1', diff --git a/app/views/nonprofits/recurring_donations/index.html.erb b/app/views/nonprofits/recurring_donations/index.html.erb index 775071b5..686b11c6 100644 --- a/app/views/nonprofits/recurring_donations/index.html.erb +++ b/app/views/nonprofits/recurring_donations/index.html.erb @@ -9,8 +9,6 @@ <%= IncludeAsset.js '/client/js/nonprofits/recurring_donations/index/page.js' %> <% end %> -<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %> - <%= render 'nonprofits/transaction_title', active: :recurring, icon_class: 'icon-return', diff --git a/app/views/nonprofits/show.html.erb b/app/views/nonprofits/show.html.erb index d83fb49c..00ea9ae4 100755 --- a/app/views/nonprofits/show.html.erb +++ b/app/views/nonprofits/show.html.erb @@ -59,8 +59,6 @@ <%= render 'admin_top_nav' %> <% end %> -<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %> - <%= render 'components/fundraising_pages/header', image_url: @nonprofit_background_image, is_editor: current_nonprofit_user?, diff --git a/app/views/nonprofits/supporters/index.html.erb b/app/views/nonprofits/supporters/index.html.erb index a117136a..00c87d36 100644 --- a/app/views/nonprofits/supporters/index.html.erb +++ b/app/views/nonprofits/supporters/index.html.erb @@ -40,8 +40,6 @@ <% end %> -<%= render '/components/trial_bar' if nonprofit_in_trial?(@nonprofit.id) %> - <%= render 'header'%> <%= render 'table_meta' %> diff --git a/app/views/settings/_pricing.html.erb b/app/views/settings/_pricing.html.erb index ec2617b6..644ca6ac 100644 --- a/app/views/settings/_pricing.html.erb +++ b/app/views/settings/_pricing.html.erb @@ -15,14 +15,8 @@
Our processor (Stripe) assesses an additional 2.2% + $0.30 for each online payment.

- <% if @nonprofit.billing_subscription.status == 'trialing' %> -

- To ensure that your account stays active after your trial, - add a payment method. -

- <% end %> - <% if @nonprofit.billing_plan.amount > 0 && @nonprofit.billing_subscription.status != 'trialing' %> + <% if @nonprofit.billing_plan.amount > 0 %>

Unsubscribe from plan

diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index d70d4f12..86579f3a 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -14,8 +14,6 @@ <%= IncludeAsset.js '/client/js/settings/index/page.js' %> <% end %> -<%= render '/components/trial_bar' if @nonprofit && nonprofit_in_trial?(@nonprofit.id) %> -

diff --git a/app/views/tickets/index.html.erb b/app/views/tickets/index.html.erb index 65d79d87..e31ab164 100644 --- a/app/views/tickets/index.html.erb +++ b/app/views/tickets/index.html.erb @@ -18,8 +18,6 @@ <%= stylesheet_link_tag 'tickets/index/page' %> <% end %> -<%= render '/components/trial_bar' if QueryBillingSubscriptions.currently_in_trial?(@nonprofit.id) %> -
diff --git a/client/js/nonprofits/cards/edit/index.es6 b/client/js/nonprofits/cards/edit/index.es6 index 626f34b3..7403cf75 100644 --- a/client/js/nonprofits/cards/edit/index.es6 +++ b/client/js/nonprofits/cards/edit/index.es6 @@ -38,9 +38,6 @@ const view = state => h('div.u-centered.u-maxWidth--600.u-margin--auto.u-marginTop--50.u-padding--15.js-view-confirm', [ h('h4', `Payment Method for ${app.nonprofit.name}`) , state.card.name ? h('p', `Current card: ${state.card.name}`) : '' - , h('p', [ - state.subscription.status === 'trialing' ? `You have ${state.daysLeft} days left in your free trial. If you add a payment method now, your account will stay active after your trial, and you will get your remaining trial days for free.` : '' - ]) , h('p.u-strong', `Tier: ${state.plan.name} ($${format.centsToDollars(state.plan.amount)} ${state.plan.interval})`) , h('hr') , h('h5', 'Update Your Card:') diff --git a/lib/construct/construct_billing_subscription.rb b/lib/construct/construct_billing_subscription.rb index 06627966..846da585 100644 --- a/lib/construct/construct_billing_subscription.rb +++ b/lib/construct/construct_billing_subscription.rb @@ -8,11 +8,9 @@ module ConstructBillingSubscription def self.with_stripe(np, billing_plan) raise ArgumentError, 'Billing plan not found' if billing_plan.nil? - trial_end = QueryBillingSubscriptions.currently_in_trial?(np.id) ? (np.created_at + 15.days).to_i : nil customer = Stripe::Customer.retrieve np.active_card.stripe_customer_id stripe_subscription = customer.subscriptions.create( - plan: billing_plan.stripe_plan_id, - trial_end: trial_end + plan: billing_plan.stripe_plan_id ) { billing_plan_id: billing_plan.id, diff --git a/lib/insert/insert_billing_subscriptions.rb b/lib/insert/insert_billing_subscriptions.rb deleted file mode 100644 index be663533..00000000 --- a/lib/insert/insert_billing_subscriptions.rb +++ /dev/null @@ -1,30 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'qx' -require 'delayed_job_helper' -require 'active_support/core_ext' - -module InsertBillingSubscriptions - def self.trial(np_id, stripe_plan_id) - nonprofit = Nonprofit.includes(:billing_subscription).find(np_id) - billing_plan = BillingPlan.where('stripe_plan_id = ?', stripe_plan_id).last - sub = nonprofit.create_billing_subscription(billing_plan: billing_plan, status: 'trialing') - n = 10 - DelayedJobHelper.enqueue_job(self, :check_trial, [sub['id']], run_at: n.days.from_now) - { json: sub } - rescue ActiveRecord::RecordNotFound => e - { json: { error: e }, status: :unprocessable_entity } - end - - def self.check_trial(bs_id) - sub = Qx.fetch(:billing_subscriptions, bs_id).last - if sub['status'] == 'trialing' - Qx.update(:billing_subscriptions) - .set(status: 'inactive') - .timestamps - .where('id = $id', id: bs_id) - .execute - end - end -end diff --git a/lib/query/query_billing_subscriptions.rb b/lib/query/query_billing_subscriptions.rb index 434b7b57..549e7b65 100644 --- a/lib/query/query_billing_subscriptions.rb +++ b/lib/query/query_billing_subscriptions.rb @@ -5,22 +5,10 @@ require 'qx' require 'active_support/core_ext' module QueryBillingSubscriptions - def self.days_left_in_trial(np_id) - sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last - return 0 if sub.nil? - - sub['status'] == 'trialing' ? (((sub['created_at'] + 10.days) - Time.current) / 86_400).floor : 0 - end - def self.plan_tier(np_id) sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last return 2 if sub && sub['status'] != 'inactive' 0 end - - def self.currently_in_trial?(np_id) - sub = Qx.fetch(:billing_subscriptions, nonprofit_id: np_id).last - sub && sub['status'] == 'trialing' - end end diff --git a/lib/update/update_billing_subscriptions.rb b/lib/update/update_billing_subscriptions.rb deleted file mode 100644 index 2a73e813..00000000 --- a/lib/update/update_billing_subscriptions.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later - -module UpdateBillingSubscriptions - def self.activate_from_trial(np_id) - Qx.update(:billing_subscriptions) - .set(status: 'active') - .timestamps - .where('nonprofit_id=$id', id: np_id) - .execute - end -end diff --git a/spec/controllers/billing_subscriptions_spec.rb b/spec/controllers/billing_subscriptions_spec.rb index 47088338..493080db 100644 --- a/spec/controllers/billing_subscriptions_spec.rb +++ b/spec/controllers/billing_subscriptions_spec.rb @@ -7,9 +7,6 @@ require 'controllers/support/shared_user_context' describe BillingSubscriptionsController, type: :controller do describe 'authorization' do include_context :shared_user_context - describe 'create_trial' do - include_context :open_to_np_admin, :post, :create_trial, nonprofit_id: :__our_np - end describe 'create' do include_context :open_to_np_admin, :post, :create, nonprofit_id: :__our_np diff --git a/spec/lib/insert/insert_billing_subscriptions_spec.rb b/spec/lib/insert/insert_billing_subscriptions_spec.rb deleted file mode 100644 index 8ae002ef..00000000 --- a/spec/lib/insert/insert_billing_subscriptions_spec.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'rails_helper' - -describe InsertBillingSubscriptions, skip: true do - let(:sub) do - # billing_plan = Qx.insert_into(:billing_plans).values({name: 'test_bp', amount: 0, stripe_plan_id: 'stripe_bp', created_at: Time.current, updated_at: Time.current}).returning('*').execute.last - # InsertBillingSubscriptions.trial(3624, billing_plan['stripe_plan_id'])[:json] - end - - describe '.trial' do - it 'creates the record' do - sub - expect(sub['id']).to be_present - end - end - - describe '.check_trial' do - it 'marks as inactive after 10 days' do - sub - Timecop.freeze(10.days.from_now) { InsertBillingSubscriptions.check_trial(sub['id']) } - updated = Qx.fetch(:billing_subscriptions, sub['id']).last - expect(updated['status']).to eq('inactive') - end - - it 'does not change the status if not still trialing after 10 days' do - sub - Qx.update(:billing_subscriptions).set(status: 'active').where('id = $id', id: sub['id']).execute - Timecop.freeze(10.days.from_now) { InsertBillingSubscriptions.check_trial(sub['id']) } - updated = Qx.fetch(:billing_subscriptions, sub['id']).last - expect(updated['status']).to eq('active') - end - end -end diff --git a/spec/lib/query/query_billing_subscriptions_spec.rb b/spec/lib/query/query_billing_subscriptions_spec.rb index d1fa0aec..866df9a3 100644 --- a/spec/lib/query/query_billing_subscriptions_spec.rb +++ b/spec/lib/query/query_billing_subscriptions_spec.rb @@ -7,36 +7,9 @@ require 'query/query_billing_subscriptions' describe QueryBillingSubscriptions, pending: true do before(:each) do - # Qx.delete_from(:billing_plans).where("stripe_plan_id = $id", id: 'stripe_bp').execute - # Qx.delete_from(:billing_subscriptions).where("nonprofit_id = $id", id: 3624).execute - # @billing_plan = Qx.insert_into(:billing_plans).values({name: 'test_bp', amount: 0, stripe_plan_id: 'stripe_bp', created_at: Time.current, updated_at: Time.current}).returning('*').execute.last - # @sub = InsertBillingSubscriptions.trial(3624, @billing_plan['stripe_plan_id'])[:json] - end - - describe '.days_left_in_trial' do - it 'gives days left in trial, rounded down' do - expect(QueryBillingSubscriptions.days_left_in_trial(3624)).to eq(9) - raise - end - - it 'gives 0 if not trialing' do - Qx.update(:billing_subscriptions).set(status: 'active').where('id = $id', id: @sub['id']).execute - expect(QueryBillingSubscriptions.days_left_in_trial(3624)).to eq(0) - raise - end - - it 'gives negative if past expiration' do - Qx.update(:billing_subscriptions).set(status: 'trialing', created_at: 20.days.ago).where('id = $id', id: @sub['id']).execute - expect(QueryBillingSubscriptions.days_left_in_trial(3624)).to eq(-11) - raise - end end describe '.plan_tier' do - it 'gives tier 2 if status=trialing' do - Qx.update(:billing_subscriptions).set(status: 'trialing').where('id = $id', id: @sub['id']).execute - expect(QueryBillingSubscriptions.plan_tier(3624)).to eq(2) - end it 'gives tier 0 if status=inactive' do Qx.update(:billing_subscriptions).set(status: 'inactive').where('id = $id', id: @sub['id']).execute expect(QueryBillingSubscriptions.plan_tier(3624)).to eq(0) @@ -52,23 +25,4 @@ describe QueryBillingSubscriptions, pending: true do raise end end - - describe '.currently_in_trial?' do - it 'gives true if status=trialing' do - Qx.update(:billing_subscriptions).set(status: 'trialing').where('id = $id', id: @sub['id']).execute - expect(QueryBillingSubscriptions.currently_in_trial?(3624)).to eq(true) - raise - end - - it 'gives false if status!=trialing' do - Qx.update(:billing_subscriptions).set(status: 'active').where('id = $id', id: @sub['id']).execute - expect(QueryBillingSubscriptions.currently_in_trial?(3624)).to eq(false) - raise - end - - it 'gives false if no subscription' do - expect(QueryBillingSubscriptions.currently_in_trial?(666)).to be_falsey - raise - end - end end From 0b6793c8a2d71c88e1c8714051cb6f8ee343eac4 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 5 Nov 2019 14:06:29 -0600 Subject: [PATCH 132/440] remove old database rake override --- lib/tasks/database.rake | 33 --------------------------------- 1 file changed, 33 deletions(-) delete mode 100644 lib/tasks/database.rake diff --git a/lib/tasks/database.rake b/lib/tasks/database.rake deleted file mode 100644 index 3f6a4a3b..00000000 --- a/lib/tasks/database.rake +++ /dev/null @@ -1,33 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Rake::Task['db:structure:dump'].clear -namespace :db do - namespace :structure do - desc 'Overriding the task db:structure:dump task to remove -i option from pg_dump to make postgres 9.5 compatible' - task dump: %i[environment load_config] do - config = ActiveRecord::Base.configurations[Rails.env] - set_psql_env(config) - filename = File.join(Rails.root, 'db', 'structure.sql') - database = config['database'] - command = "pg_dump -s -x -O -f #{Shellwords.escape(filename)} #{Shellwords.escape(database)}" - raise 'Error dumping database' unless Kernel.system(command) - - File.open(filename, 'a') { |f| f << "SET search_path TO #{ActiveRecord::Base.connection.schema_search_path};\n\n" } - if ActiveRecord::Base.connection.supports_migrations? - File.open(filename, 'a') do |f| - f.puts ActiveRecord::Base.connection.dump_schema_information - f.print "\n" - end - end - Rake::Task['db:structure:dump'].reenable - end - end - - def set_psql_env(configuration) - ENV['PGHOST'] = configuration['host'] if configuration['host'] - ENV['PGPORT'] = configuration['port'].to_s if configuration['port'] - ENV['PGPASSWORD'] = configuration['password'].to_s if configuration['password'] - ENV['PGUSER'] = configuration['username'].to_s if configuration['username'] - end -end From 53b3f5800e7431b3c082516368164cd4174dcfec Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 5 Nov 2019 13:51:25 -0600 Subject: [PATCH 133/440] Removing tiers --- app/controllers/application_controller.rb | 10 - .../nonprofits/supporters_controller.rb | 1 - app/views/common/_onboarding_modals.html.erb | 42 - app/views/layouts/_app_data.html.erb | 1 - app/views/nonprofits/_actions.html.erb | 2 +- app/views/nonprofits/donate.html.erb | 1 - client/js/common/application_view.js | 8 - client/js/nonprofits/donate/page.js | 18 +- client/js/settings/index/branding/view.js | 1 - ...0191105200033_remove_billing_plan_tiers.rb | 5 + db/structure.sql | 1416 ++++++----------- lib/controllers/nonprofit_helper.rb | 6 - lib/query/query_billing_subscriptions.rb | 14 - .../query/query_billing_subscriptions_spec.rb | 28 - 14 files changed, 487 insertions(+), 1066 deletions(-) delete mode 100644 app/views/common/_onboarding_modals.html.erb create mode 100644 db/migrate/20191105200033_remove_billing_plan_tiers.rb delete mode 100644 lib/query/query_billing_subscriptions.rb delete mode 100644 spec/lib/query/query_billing_subscriptions_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 892ee424..009ceab5 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -124,16 +124,6 @@ class ApplicationController < ActionController::Base QueryRoles.user_has_role?(current_user.id, role_names, host_id) end - def current_plan_tier(npo_id = nil) - return 0 if !npo_id && !administered_nonprofit - - npo_id ||= administered_nonprofit.id - return 2 if current_role?(:super_admin) - - key = "plan_tier_user_#{current_user_id}_nonprofit_#{npo_id}" - administered_nonprofit ? QueryBillingSubscriptions.plan_tier(npo_id) : 0 - end - def administered_nonprofit return nil unless current_user diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index 250202e2..92aa169a 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -6,7 +6,6 @@ module Nonprofits include Controllers::NonprofitHelper before_action :authenticate_nonprofit_user!, except: %i[new create] - # before_action(except: [:create, :mailchimp_landing]){authenticate_min_nonprofit_plan(2)} # get /nonprofit/:nonprofit_id/supporters def index diff --git a/app/views/common/_onboarding_modals.html.erb b/app/views/common/_onboarding_modals.html.erb deleted file mode 100644 index e0adb149..00000000 --- a/app/views/common/_onboarding_modals.html.erb +++ /dev/null @@ -1,42 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> - - -<%= render 'billing_subscriptions/new_modal' %> - -<%= render 'nonprofits/new_modal' unless current_role?(:nonprofit_associate) || current_role?(:nonprofit_admin) %> - - - - - - - diff --git a/app/views/layouts/_app_data.html.erb b/app/views/layouts/_app_data.html.erb index c6d273d1..69033f64 100644 --- a/app/views/layouts/_app_data.html.erb +++ b/app/views/layouts/_app_data.html.erb @@ -7,7 +7,6 @@ var app = { , current_admin: <%= !!(current_user && current_role?(:super_admin)) %> , nonprofit: <%= @nonprofit ? raw(@nonprofit.to_json) : 'undefined' %> , nonprofit_id : <%= @nonprofit ? @nonprofit.id : 'undefined' %> -, current_plan_tier: <%= @nonprofit ? current_plan_tier(@nonprofit.id) : 'undefined' %> , user: <%= current_user ? raw(current_user.to_json) : 'undefined' %> , user_id: <%= current_user ? current_user.id : 'undefined' %> , profile: <%= current_user ? raw(current_user.profile.to_json) : 'undefined' %> diff --git a/app/views/nonprofits/_actions.html.erb b/app/views/nonprofits/_actions.html.erb index dec57f93..4cc0dc35 100644 --- a/app/views/nonprofits/_actions.html.erb +++ b/app/views/nonprofits/_actions.html.erb @@ -13,7 +13,7 @@ <% end %> - <% if current_plan_tier >= 1 && current_nonprofit_user? %> + <% if current_nonprofit_user? %> diff --git a/app/views/nonprofits/donate.html.erb b/app/views/nonprofits/donate.html.erb index dea08c92..1924d6d1 100755 --- a/app/views/nonprofits/donate.html.erb +++ b/app/views/nonprofits/donate.html.erb @@ -9,7 +9,6 @@ <% content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/campaigns/index/page.js' %> + <%= javascript_packs_with_chunks_tag 'page__', 'page__campaigns__index' %> <% end %> <%= render 'components/header', diff --git a/app/views/layouts/_javascripts.html.erb b/app/views/layouts/_javascripts.html.erb index 6ca0c96c..f86da7d9 100644 --- a/app/views/layouts/_javascripts.html.erb +++ b/app/views/layouts/_javascripts.html.erb @@ -14,8 +14,6 @@ Stripe.setPublishableKey("<%= Settings.payment_provider.stripe_public_key %>"); window._csrf = "<%= form_authenticity_token %>"; -<%= IncludeAsset.js "/client/js/page.js" %> - <%= IncludeAsset.js '/client/js/i18n.js' %> - <%= IncludeAsset.js '/client/js/i18n.js' %> - <%= IncludeAsset.js '/client/js/nonprofits/donate/page.js' %> <% end %> <% content_for :stylesheets do %> From 564711f1741f86b1b5255205a9a91d3f6b9df9ad Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 27 Nov 2019 17:05:38 -0600 Subject: [PATCH 266/440] Move widget css into a controller so it's updateable --- .../assets/stylesheets/widget/donate-button-v2.css | 0 .../assets/stylesheets/widget}/donate-button.css | 0 app/controllers/widget_controller.rb | 10 ++++++++++ config/initializers/assets.rb | 2 +- config/routes.rb | 3 +++ 5 files changed, 14 insertions(+), 1 deletion(-) rename public/css/donate-button.v2.css => app/assets/stylesheets/widget/donate-button-v2.css (100%) rename {public/css => app/assets/stylesheets/widget}/donate-button.css (100%) diff --git a/public/css/donate-button.v2.css b/app/assets/stylesheets/widget/donate-button-v2.css similarity index 100% rename from public/css/donate-button.v2.css rename to app/assets/stylesheets/widget/donate-button-v2.css diff --git a/public/css/donate-button.css b/app/assets/stylesheets/widget/donate-button.css similarity index 100% rename from public/css/donate-button.css rename to app/assets/stylesheets/widget/donate-button.css diff --git a/app/controllers/widget_controller.rb b/app/controllers/widget_controller.rb index a3ed8bf6..1ef940bc 100644 --- a/app/controllers/widget_controller.rb +++ b/app/controllers/widget_controller.rb @@ -7,4 +7,14 @@ class WidgetController < ApplicationController def i18n head :found, location: helpers.asset_pack_url("i18n.js"), content_type: "application/javascript" end + + def v1_css + expires_in 10.minutes + head :found, location: helpers.stylesheet_url("widget/donate-button.css"), content_type: "text/css" + end + + def v2_css + expires_in 10.minutes + head :found, location: helpers.stylesheet_url("widget/donate-button-v2.css"), content_type: "text/css" + end end diff --git a/config/initializers/assets.rb b/config/initializers/assets.rb index 678efe9f..cf6c0819 100644 --- a/config/initializers/assets.rb +++ b/config/initializers/assets.rb @@ -10,4 +10,4 @@ Rails.application.config.assets.version = '1.0' # Precompile additional assets. # application.js, application.css, and all non-JS/CSS in app/assets folder are already added. -# Rails.application.config.assets.precompile += %w( search.js ) +Rails.application.config.assets.precompile += %w( widget/donate-button.css widget/donate-button-v2.css) diff --git a/config/routes.rb b/config/routes.rb index 3ae70be2..86effd64 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -259,5 +259,8 @@ Rails.application.routes.draw do get '/js/donate-button.v2.js' => 'widget#v2' get '/js/i18n.js' => 'widget#i18n' + get '/css/donate-button.css' => 'widget#v1_css' + get '/css/donate-button.v2.css' => 'widget#v2_css' + root to: 'front#index' end \ No newline at end of file From a1e7d76ddd9e00932c89ab097c1fe7ddd083b1a0 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Wed, 4 Dec 2019 16:22:48 -0600 Subject: [PATCH 267/440] modified a lot of things --- .../stylesheets/experiment/global/buttons.css | 40 ++++++++ .../stylesheets/experiment/global/colors.css | 45 +++++++++ .../experiment/global/confirmation.css | 14 +++ .../experiment/global/containers.css | 10 ++ .../experiment/global/dashboard.css | 31 +++++++ .../experiment/global/decorative.css | 29 ++++++ .../experiment/global/form-elements.css | 84 +++++++++++++++++ .../stylesheets/experiment/global/icons.css | 83 +++++++++++++++++ .../stylesheets/experiment/global/loader.css | 24 +++++ .../stylesheets/experiment/global/modal.css | 92 +++++++++++++++++++ .../experiment/global/notification.css | 25 +++++ .../stylesheets/experiment/global/page.css | 25 +++++ .../stylesheets/experiment/global/pre.css | 10 ++ .../stylesheets/experiment/global/shadows.css | 12 +++ .../stylesheets/experiment/global/tabswap.css | 49 ++++++++++ .../stylesheets/experiment/global/tooltip.css | 51 ++++++++++ .../experiment/global/typography.css | 58 ++++++++++++ .../stylesheets/experiment/global/utils.css | 5 + .../stylesheets/experiment/global/wizard.css | 91 ++++++++++++++++++ .../legacy/components/nonprofit-branding.js | 2 +- .../packs/page__bank_accounts__confirm.js | 2 + .../packs/page__campaigns__index.js | 2 + .../packs/page__campaigns__peer_to_peer.js | 2 + app/javascript/packs/page__campaigns__show.js | 2 + .../page__campaigns__supporters__index.js | 2 + app/javascript/packs/page__events__index.js | 2 + app/javascript/packs/page__events__show.js | 2 + app/javascript/packs/page__events__stats.js | 2 + app/javascript/packs/page__nonprofits__btn.js | 2 + .../packs/page__nonprofits__button.js | 2 + .../packs/page__nonprofits__cards__edit.js | 2 + .../packs/page__nonprofits__dashboard.js | 2 + .../packs/page__nonprofits__donate.js | 1 + .../packs/page__nonprofits__edit.js | 2 + .../page__nonprofits__payments__index.js | 2 + .../packs/page__nonprofits__payouts__index.js | 2 + ..._nonprofits__recurring_donations__index.js | 2 + .../packs/page__nonprofits__show.js | 2 + .../packs/page__nonprofits__supporter_form.js | 2 + .../page__nonprofits__supporters__index.js | 1 + .../page__nonprofits__supporters__new.js | 2 + .../packs/page__recurring_donations__edit.js | 2 + app/javascript/packs/page__settings__index.js | 2 + app/javascript/packs/page__super-admin.js | 2 + app/javascript/packs/page__tickets__index.js | 2 + app/javascript/packs/registration_page.js | 1 - app/models/nonprofit.rb | 3 +- app/views/campaigns/index.html.erb | 2 +- app/views/campaigns/peer_to_peer.html.erb | 2 +- app/views/campaigns/show.html.erb | 2 +- app/views/campaigns/supporters/index.html.erb | 2 +- app/views/devise/sessions/new.html.erb | 2 +- app/views/events/index.html.erb | 2 +- app/views/events/show.html.erb | 2 +- app/views/events/stats.html.erb | 2 +- app/views/layouts/_footer.html.erb | 4 - app/views/layouts/_javascripts.html.erb | 12 +-- app/views/layouts/page.html.erb | 2 +- .../bank_accounts/confirmation.html.erb | 2 +- app/views/nonprofits/btn.html.erb | 2 +- app/views/nonprofits/button/advanced.html.erb | 2 +- app/views/nonprofits/button/guided.html.erb | 2 +- app/views/nonprofits/cards/edit.html.erb | 2 +- app/views/nonprofits/dashboard.html.erb | 4 +- app/views/nonprofits/payments/index.html.erb | 9 +- app/views/nonprofits/payouts/index.html.erb | 2 +- .../recurring_donations/index.html.erb | 2 +- app/views/nonprofits/show.html.erb | 3 +- app/views/nonprofits/supporter_form.html.erb | 2 +- .../nonprofits/supporters/index.html.erb | 7 +- app/views/nonprofits/supporters/new.html.erb | 2 +- app/views/recurring_donations/edit.html.erb | 2 +- app/views/settings/index.html.erb | 2 +- app/views/super_admins/index.html.erb | 2 +- app/views/tickets/index.html.erb | 2 +- config/commitchange.yml | 2 +- 76 files changed, 865 insertions(+), 52 deletions(-) create mode 100644 app/assets/stylesheets/experiment/global/buttons.css create mode 100644 app/assets/stylesheets/experiment/global/colors.css create mode 100644 app/assets/stylesheets/experiment/global/confirmation.css create mode 100644 app/assets/stylesheets/experiment/global/containers.css create mode 100644 app/assets/stylesheets/experiment/global/dashboard.css create mode 100644 app/assets/stylesheets/experiment/global/decorative.css create mode 100644 app/assets/stylesheets/experiment/global/form-elements.css create mode 100644 app/assets/stylesheets/experiment/global/icons.css create mode 100644 app/assets/stylesheets/experiment/global/loader.css create mode 100644 app/assets/stylesheets/experiment/global/modal.css create mode 100644 app/assets/stylesheets/experiment/global/notification.css create mode 100644 app/assets/stylesheets/experiment/global/page.css create mode 100644 app/assets/stylesheets/experiment/global/pre.css create mode 100644 app/assets/stylesheets/experiment/global/shadows.css create mode 100644 app/assets/stylesheets/experiment/global/tabswap.css create mode 100644 app/assets/stylesheets/experiment/global/tooltip.css create mode 100644 app/assets/stylesheets/experiment/global/typography.css create mode 100644 app/assets/stylesheets/experiment/global/utils.css create mode 100644 app/assets/stylesheets/experiment/global/wizard.css diff --git a/app/assets/stylesheets/experiment/global/buttons.css b/app/assets/stylesheets/experiment/global/buttons.css new file mode 100644 index 00000000..b9e0bbb8 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/buttons.css @@ -0,0 +1,40 @@ +/* License: LGPL-3.0-or-later */ +.btn, +button { + border-width: 2px; + border-style: solid; + border-color: var(--blue); + color: var(--blue); + background: white; + transition: color 100ms ease, background 100ms ease, border-color 100ms ease; + padding: .25em .5em; +} + +.btn-main, +.btn:hover, +button:hover { + color: white; + background: var(--blue); +} + +[data-ff-confirmation-button="yes"], +.btn-danger { + border-color: var(--red); + color: var(--red); +} + +[data-ff-confirmation-button="yes"]:hover, +.btn-danger:hover { + background: var(--red); +} + +.btn-main:hover { + border-color: color(var(--blue) l(40%)); + background-color: color(var(--blue) l(40%)); +} + +.buttons [class*=btn]:last-of-type, +.buttons button:last-of-type { + border-radius: 0 3px 3px 0; + border-right-width: 2px; +} diff --git a/app/assets/stylesheets/experiment/global/colors.css b/app/assets/stylesheets/experiment/global/colors.css new file mode 100644 index 00000000..c8ddb442 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/colors.css @@ -0,0 +1,45 @@ +/* License: LGPL-3.0-or-later */ +:root { + --grey-05: color(black l(99.4%)); + --grey-1: color(black l(98%)); + --grey-2: color(black l(96%)); + --grey-3: color(black l(92%)); + --grey-4: color(black l(86%)); + --grey-5: color(black l(65%)); + --grey-6: color(black l(50%)); + --black: #2f2f2f; + + --scrim: rgba(0,0,0,.5); + + --blue: #0176f5; + --blue-light: #78b6ff; + --blue-lighter: #d2e6ff; + --green: #82c13e; + --red: #ff0f0f; +} + +.bg-grey-1 { background: var(--grey-1)} +.bg-grey-2 { background: var(--grey-2)} +.bg-grey-3 { background: var(--grey-3)} +.bg-grey-4 { background: var(--grey-4)} +.bg-grey-5 { background: var(--grey-5)} +.bg-scrim { background: var(--scrim)} +.bg-black { background: var(--black)} +.bg-blue { background: var(--blue)} +.bg-blue-light { background: var(--blue-light)} +.bg-blue-lighter { background: var(--blue-lighter)} +.bg-red { background: var(--red)} +.bg-green { background: var(--green)} + +.color-white { color: white} +.color-grey { color: var(--grey-6)} +.color-black { color: var(--black)} +.color-red { color: var(--red)} +.color-green { color: var(--green)} +.color-blue { color: var(--blue)} + +.border-color-grey { border-color: var(--grey-4)} +.border-color-red { border-color: var(--red)} +.border-color-green { border-color: var(--green)} +.border-color-blue { border-color: var(--blue-light)} + diff --git a/app/assets/stylesheets/experiment/global/confirmation.css b/app/assets/stylesheets/experiment/global/confirmation.css new file mode 100644 index 00000000..41160fbc --- /dev/null +++ b/app/assets/stylesheets/experiment/global/confirmation.css @@ -0,0 +1,14 @@ +/* License: LGPL-3.0-or-later */ +[data-ff-confirmation-prompt] { + font-weight: 500; + margin-top: 0; + font-size: 1.5rem; + margin-bottom: 2rem; + padding-right: 2rem; +} + +[data-ff-confirmation-button="yes"] { + margin-right: 1rem; +} + + diff --git a/app/assets/stylesheets/experiment/global/containers.css b/app/assets/stylesheets/experiment/global/containers.css new file mode 100644 index 00000000..45946542 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/containers.css @@ -0,0 +1,10 @@ +/* License: LGPL-3.0-or-later */ +/*[class*="container"] {*/ + /*margin-left: auto;*/ + /*margin-right: auto;*/ +/*}*/ + +/*.container { max-width: 60rem; }*/ +/*.container--medium { max-width: 50rem; }*/ +/*.container--narrow { max-width: 40rem; }*/ + diff --git a/app/assets/stylesheets/experiment/global/dashboard.css b/app/assets/stylesheets/experiment/global/dashboard.css new file mode 100644 index 00000000..ba357de2 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/dashboard.css @@ -0,0 +1,31 @@ +/* License: LGPL-3.0-or-later */ +@import 'ff-dashboard'; +@import './colors.css'; + +[data-ff-dashboard-header] { + background: white; + padding: .5rem 1rem; + border-bottom: 2px solid var(--grey-4); +} + +[data-ff-dashboard-main-panel] { + background: var(--grey-05); +} + +[data-ff-dashboard-left-panel], +[data-ff-dashboard-right-panel] { + background: white; +} + +[data-ff-dashboard-left-panel] { + border-right: 2px solid var(--grey-4); +} + +[data-ff-dashboard-right-panel] { + border-left: 2px solid var(--grey-4); +} + +[data-ff-dashboard-panel-body] { + overflow-x: auto; +} + diff --git a/app/assets/stylesheets/experiment/global/decorative.css b/app/assets/stylesheets/experiment/global/decorative.css new file mode 100644 index 00000000..bc722f89 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/decorative.css @@ -0,0 +1,29 @@ +/* License: LGPL-3.0-or-later */ +ul.tabs--v li.is-selected, +ul.tabs--h li.is-selected { + border-color: var(--green); +} + +ul.tabs--h li, +ul.tabs--v li { + color: var(--black); +} + +.border, +.border-top, +.border-bottom, +.border-left, +.border-right { + border-width: 2px; +} + +hr { + border-bottom-width: 2px; + border-color: var(--grey-4); +} + +::selection { + color: white; + background: var(--black); +} + diff --git a/app/assets/stylesheets/experiment/global/form-elements.css b/app/assets/stylesheets/experiment/global/form-elements.css new file mode 100644 index 00000000..3a3bf896 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/form-elements.css @@ -0,0 +1,84 @@ +/* License: LGPL-3.0-or-later */ +input[type='checkbox'] + label:before, +input[type='radio'] + label:before, +select, +textarea, +input { + border: 2px solid var(--grey-5); +} + +label { + margin-bottom: .25em; +} + +select, +textarea, +input { + border-radius: 3px; +} + +textarea, +select, +input { + padding: .25em .5em; +} + +select:focus, +input:focus, +textarea:focus { + border-color: var(--green); +} + +input[type='radio'] + label, +input[type='checkbox'] + label { + font-size: 1rem; +} + +input[type='radio'] + label:before, +input[type='checkbox'] + label:before { + display: inline-block; + content: "\E876"; + margin: 0; + font-family: 'Material Icons'; + padding: 0; + font-size: .85rem; + width: 1.1rem; + line-height: 1.1rem; +} + +input[type='radio'] + label:before { + border-radius: 50%; +} + +input[type='checkbox']:checked + label:before, +input[type='radio']:checked + label:before { + border-color: var(--blue); + background: var(--blue-light); +} + +input[type='checkbox']:checked + label:before, +input[type='radio']:checked + label:before { + color: white; +} + +select, +.dollar-input { + background-position: center; + background-repeat: no-repeat; +} + +select { + -moz-appearance: none !important; + width: initial; + background-image: url('/svgs/dropdown.svg'); + background-position-x: calc(100% - .25rem); + padding-right: 2rem; +} + +.dollar-input { + background-image: url('/svgs/dollar.svg'); + background-size: 1.1rem; + background-position-x: .25rem; + padding-left: 1.5rem; +} + diff --git a/app/assets/stylesheets/experiment/global/icons.css b/app/assets/stylesheets/experiment/global/icons.css new file mode 100644 index 00000000..b0eb5cdb --- /dev/null +++ b/app/assets/stylesheets/experiment/global/icons.css @@ -0,0 +1,83 @@ +/* License: LGPL-3.0-or-later */ +@font-face { + font-family: 'Material Icons'; + font-style: normal; + src: local('/fonts/material/MaterialIcons-Regular'), + url('/fonts/material/MaterialIcons-Regular.woff2') format('woff2'), + url('/fonts/material/MaterialIcons-Regular.woff') format('woff'), + url('/fonts/material/MaterialIcons-Regular.ttf') format('truetype'); +} + +@font-face { + font-family: 'Social Icons'; + src: url('/fonts/social/fontello.eot?34099374'); + src: url('/fonts/social/fontello.eot?34099374#iefix') format('embedded-opentype'), + url('/fonts/social/fontello.woff2?34099374') format('woff2'), + url('/fonts/social/fontello.woff?34099374') format('woff'), + url('/fonts/social/fontello.ttf?34099374') format('truetype'), + url('/fonts/social/fontello.svg?34099374#fontello') format('svg'); + font-style: normal; +} + +[class*="social-icon-"]:before, +.m-icon { + font-weight: normal; + font-style: normal; + text-transform: none; + letter-spacing: normal; + word-wrap: normal; + white-space: nowrap; + direction: ltr; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; + -moz-osx-font-smoothing: grayscale; + font-feature-settings: 'liga'; +} + +/* reference: https://material.io/icons/ */ +.m-icon { + font-family: 'Material Icons'; + vertical-align: middle; +} + +[class*=".social-icon-"] { + display: inline-block; + vertical-align: middle; +} + +/* social icons include logos for the following platforms */ +/* tumbler */ +/* twitter */ +/* facebook */ +/* github */ +/* linkedin */ +/* youtube */ +/* instagram */ +/* flickr */ +/* google */ +/* vine */ +/* pinterest */ +/* whatsapp */ +/* vimeo */ +/* reddit */ +/* snapchat */ +/* quora */ + +[class*="social-icon-"]:before { font-family: "Social Icons"; } + +.social-icon-tumbler:before { content: '\e800'; } +.social-icon-twitter:before { content: '\f099'; } +.social-icon-facebook:before { content: '\f09a'; } +.social-icon-github:before { content: '\f09b'; } +.social-icon-linkedin:before { content: '\f0e1'; } +.social-icon-youtube:before { content: '\f16a'; } +.social-icon-instagram:before { content: '\f16d'; } +.social-icon-flickr:before { content: '\f16e'; } +.social-icon-google:before { content: '\f1a0'; } +.social-icon-vine:before { content: '\f1ca'; } +.social-icon-pinterest:before { content: '\f231';} +.social-icon-whatsapp:before { content: '\f232'; } +.social-icon-vimeo:before { content: '\f27d'; } +.social-icon-reddit:before { content: '\f281'; } +.social-icon-snapchat:before { content: '\f2ac'; } +.social-icon-quora:before { content: '\f2c4'; } diff --git a/app/assets/stylesheets/experiment/global/loader.css b/app/assets/stylesheets/experiment/global/loader.css new file mode 100644 index 00000000..4f5a72bc --- /dev/null +++ b/app/assets/stylesheets/experiment/global/loader.css @@ -0,0 +1,24 @@ +/* License: LGPL-3.0-or-later */ +.loader { + height: 2px; + width: 100%; + position: fixed; + top: 0; + left: 0; + z-index: 2; +} + +.loader:before{ + transform: translateZ(0); + position: absolute; + content: ""; + height: 100%; + background: var(--green); + animation: loader 1s linear infinite; +} + +@keyframes loader { + from {left: -10%; width: 10%} + 50% {width: 60%; left: 40%;} + to {left: 130%; width: 10%} +} diff --git a/app/assets/stylesheets/experiment/global/modal.css b/app/assets/stylesheets/experiment/global/modal.css new file mode 100644 index 00000000..dde3476c --- /dev/null +++ b/app/assets/stylesheets/experiment/global/modal.css @@ -0,0 +1,92 @@ +/* License: LGPL-3.0-or-later */ +@import 'flimflam/ui/modal/index.css'; /* npm */ + +[data-ff-modal-backdrop] { + z-index: 2; + transform: translateZ(0); + transition: opacity 200ms ease-out, visibility 200ms ease-out; + background: var(--scrim); +} + +[data-ff-modal-backdrop=hidden] { + display: block; + opacity: 0; + visibility: hidden; +} + +[data-ff-modal-backdrop=shown] { + opacity: 1; + visibility: visible; +} + +[data-ff-modal] { + box-shadow: var(--sh-2); +} + +[data-ff-modal-header] { padding: 1rem 3rem 1rem 1rem; } + +[data-ff-modal-body], +[data-ff-modal-footer] { + padding: 1rem; +} + +[data-ff-modal-header] , +[data-ff-modal-footer] { + background: var(--grey-1); +} + +[data-ff-modal-header] h4 { margin: 0; } + +[data-ff-modal-close-button] { + top: 1rem; + right: 1rem; +} + +[data-ff-modal-close-button]:after { + line-height: 1; + font-size: 1.75rem; + color: var(--grey-6); + content: '×'; +} + +.modal-medium [data-ff-modal] { + width: 40rem; + margin-left: -20rem; +} +[data-ff-confirmation] [data-ff-modal], +.modal-small [data-ff-modal] { + width: 30rem; + margin-left: -15rem; +} + +.modal-large [data-ff-modal] { + width: 50rem; + margin-left: -25rem; +} + +@media (max-width: 40rem) { + .modal-medium [data-ff-modal] { + width: calc(100% - 1rem); + margin-left: -0.5rem; + left: 1rem; + } +} + +@media (max-width: 50rem) { + .modal-large [data-ff-modal] { + width: calc(100% - 1rem); + margin-left: -0.5rem; + left: 1rem; + } +} + +@media (max-width: 30rem) { + [data-ff-confirmation] [data-ff-modal], + .modal-small [data-ff-modal] { + width: calc(100% - 1rem); + margin-left: -0.5rem; + left: 1rem; + } +} + + diff --git a/app/assets/stylesheets/experiment/global/notification.css b/app/assets/stylesheets/experiment/global/notification.css new file mode 100644 index 00000000..a5de1019 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/notification.css @@ -0,0 +1,25 @@ +/* License: LGPL-3.0-or-later */ +[data-ff-notification] { + position: fixed; + width: 100%; + text-align: center; + box-shadow: var(--sh-2); + background: var(--black); + color: white; + font-weight: 500; + padding: 1rem; + z-index: 1; + left: 0; + bottom: -50%; + opacity: 0; + visibility: hidden; + transform: translateZ(0); + transition: 200ms ease; +} + +[data-ff-notification=shown] { + opacity: 0.9; + visibility: visible; + bottom: 0; +} + diff --git a/app/assets/stylesheets/experiment/global/page.css b/app/assets/stylesheets/experiment/global/page.css new file mode 100644 index 00000000..09389531 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/page.css @@ -0,0 +1,25 @@ +/* License: LGPL-3.0-or-later */ +@import 'commons.css'; /* npm */ +@import 'colors.css'; /* contains variables */ +@import 'shadows.css'; /* contains variables */ +/*@import 'typography.css';*/ +@import 'icons.css'; +@import 'containers.css'; +@import 'buttons.css'; +@import 'decorative.css'; +@import 'form-elements.css'; +@import 'modal.css'; +@import 'notification.css'; +@import 'confirmation.css'; +@import 'wizard.css'; +@import 'tooltip.css'; +@import 'tabswap.css'; +@import 'utils.css'; +@import 'loader.css'; +@import 'pre.css'; + +/* add to commons */ +@media (max-width: 30rem) { + .sm-left { float: left; } +} + diff --git a/app/assets/stylesheets/experiment/global/pre.css b/app/assets/stylesheets/experiment/global/pre.css new file mode 100644 index 00000000..637f4a22 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/pre.css @@ -0,0 +1,10 @@ +/* License: LGPL-3.0-or-later */ +pre { + white-space: pre-wrap; + word-break: break-all; + font-size: .8rem; + background: var(--grey-1); + padding: 1rem; + margin: 0; +} + diff --git a/app/assets/stylesheets/experiment/global/shadows.css b/app/assets/stylesheets/experiment/global/shadows.css new file mode 100644 index 00000000..55d1aa03 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/shadows.css @@ -0,0 +1,12 @@ +/* License: LGPL-3.0-or-later */ +:root { + --sh-1: 0 1px 3px 1px rgba(0,0,0,.1); + --sh-2: 0 1.5px 6px 1px rgba(0,0,0,.2); + --sh-3: 0 2px 10px 1px rgba(0,0,0,.3); + --sh-4: 0 3px 12px 1px rgba(0,0,0,.4); +} + +/* these box-shadow variables correspond with the */ +/* shadow classes in commons.css (http://yutakahoulette.com/commons.css/#shadow) */ + + diff --git a/app/assets/stylesheets/experiment/global/tabswap.css b/app/assets/stylesheets/experiment/global/tabswap.css new file mode 100644 index 00000000..2fe7823d --- /dev/null +++ b/app/assets/stylesheets/experiment/global/tabswap.css @@ -0,0 +1,49 @@ +/* License: LGPL-3.0-or-later */ +@import 'flimflam/ui/tabswap/index.css'; /* npm */ + +[data-ff-tabswap-labels] { + border-bottom: 2px solid var(--grey-4); +} + +[data-ff-tabswap-label] { + border: 2px solid var(--grey-4); +} + +[data-ff-tabswap-label], +[data-ff-tabswap-label-wrapper] { + display: inline-block; +} + +[data-ff-tabswap-label-wrapper]:first-of-type { + margin-left: 1rem; +} + +[data-ff-tabswap-label-wrapper] { + margin-right: .5rem; +} + +[data-ff-tabswap-label] { + background: var(--grey-2); + padding: .25rem 1rem; + border-radius: 3px 3px 0 0; + transform: translateY(2px); + position: relative; + color: var(--grey-6); +} + +[data-ff-tabswap-label="active"] { + font-weight: 400; + background: white; + color: var(--blue); +} + +[data-ff-tabswap-label="active"]:after { + content: ''; + position: absolute; + left: 0; + width: 100%; + height: 2px; + bottom: -2px; + background: white; +} + diff --git a/app/assets/stylesheets/experiment/global/tooltip.css b/app/assets/stylesheets/experiment/global/tooltip.css new file mode 100644 index 00000000..76f320c1 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/tooltip.css @@ -0,0 +1,51 @@ +/* License: LGPL-3.0-or-later */ +@import 'data-tooltip'; /* npm */ + +[data-ff-field] { position: relative; } + +[data-ff-field]:before, +[data-ff-field]:after { + position: absolute; + visibility: hidden; + pointer-events: none; + box-sizing: border-box; +} + +[data-ff-field='invalid']:before, +[data-ff-field='invalid']:after { + visibility: visible; +} + +[data-ff-field]:before { + content: ''; + position: absolute; + background: transparent; + height: 6px; + width: 6px; + border: 6px solid transparent; + border-top-color: #383838; + bottom: calc(100% - 6px); + left: 12px; +} + +[data-ff-field]:after { + background: #383838; + font-weight: normal; + color: white; + padding: 6px 8px; + line-height: 1.4; + white-space: nowrap; + border-radius: 3px; + text-align: left; + content: attr(data-ff-field-error); + bottom: calc(100% + 6px); + left: 0; +} + +[data-ff-field]:after, +[class*=tooltip--]:after { + font-size: .75rem; + letter-spacing: 1px; + box-shadow: 1px 1px 4px 1px rgba(0,0,0,.1); +} + diff --git a/app/assets/stylesheets/experiment/global/typography.css b/app/assets/stylesheets/experiment/global/typography.css new file mode 100644 index 00000000..9fc1fc42 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/typography.css @@ -0,0 +1,58 @@ +/* License: LGPL-3.0-or-later */ +@font-face { + font-family: 'Open Sans'; + src: url('/fonts/Open_Sans/opensans-regular-webfont.ttf') format('truetype'); + font-weight: 400; + font-style: normal; +} +@font-face { + font-family: 'Open Sans'; + src: url('/fonts/Open_Sans/OpenSans-Semibold.ttf') format('truetype'); + font-weight: 500; + font-style: normal; +} +@font-face { + font-family: 'Open Sans'; + src: url('/fonts/Open_Sans/opensans-bold-webfont.ttf') format('truetype'); + font-weight: 600; + font-style: normal; +} + +body { + font-family: + OpenSans, + -apple-system, + BlinkMacSystemFont, + Segoe UI, + Helvetica Neue, + Helvetica, + sans-serif; + line-height: 1.5; + color: var(--black); +} + +a { + color: inherit; + text-decoration: none; +} + +p a { + background-image: linear-gradient(to right, var(--grey-5), var(--grey-5)); + background-repeat: no-repeat,no-repeat,repeat-x; + background-position: 0 100%; + background-size: 100% 1px; + display: inline; +} + +.jumbo { font-size: 4rem; } +.bump { font-size: 1.2rem; } +.sub { font-size: .9rem; } + +label { font-weight: 500; } + +strong, .bold, h1, h2, h3, h4, h5, h6 { font-weight: 600; } + +.font-weight-1 { font-weight: 400; } +.font-weight-2 { font-weight: 500; } +.font-weight-3 { font-weight: 600; } + diff --git a/app/assets/stylesheets/experiment/global/utils.css b/app/assets/stylesheets/experiment/global/utils.css new file mode 100644 index 00000000..c792c4b7 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/utils.css @@ -0,0 +1,5 @@ +/* License: LGPL-3.0-or-later */ +.transition-slow { + transition: 300ms ease; +} + diff --git a/app/assets/stylesheets/experiment/global/wizard.css b/app/assets/stylesheets/experiment/global/wizard.css new file mode 100644 index 00000000..e7abeaa9 --- /dev/null +++ b/app/assets/stylesheets/experiment/global/wizard.css @@ -0,0 +1,91 @@ +/* License: LGPL-3.0-or-later */ +[data-ff-wizard-label]:first-of-type:before { + display: none; +} + +[data-ff-wizard-label] { + opacity: 1; + transition: color 200ms ease; +} + + + +[data-ff-wizard-label='accessible'] { cursor: pointer; } + +[data-ff-wizard-label='inaccessible'] { cursor: default; } + +[data-ff-wizard-label] { + text-align: center; + font-size: .8rem; + padding: 1rem .25rem 0 .25rem; + text-align: center; + position: relative; + display: inline-block; +} + +[data-ff-wizard-label]:after, +[data-ff-wizard-label]:before { + content: ''; + position: absolute; + transition: background 200ms ease; +} + +[data-ff-wizard-label]:after { + z-index: 1; + top: 0; + right: 50%; + transform: translateX(.5rem); + height: .8rem; + width: .8rem; + border-radius: 2rem; +} + +[data-ff-wizard-label]:before { + height: .2rem; + width: 100%; + top: .35rem; + right: 50%; +} + +[data-ff-wizard-label-wrapper='complete'] [data-ff-wizard-label] { + color: var(--grey-5); + pointer-events: none; +} + + +[data-ff-wizard-label] { + color: var(--grey-5); +} + +[data-ff-wizard-label-wrapper='complete'] [data-ff-wizard-label]:after, +[data-ff-wizard-label]:after { + background: var(--grey-5); +} + +[data-ff-wizard-label-wrapper='complete'] [data-ff-wizard-label]:before, +[data-ff-wizard-label]:before { + background: var(--grey-4); +} + +[data-ff-wizard-label='accessible']:after, +[data-ff-wizard-label='current']:after { + background: var(--blue); +} + +[data-ff-wizard-label='accessible']:before, +[data-ff-wizard-label='current']:before { + background: var(--blue-light); +} + +[data-ff-wizard-label-wrapper] { + width: 100%; + padding: 1rem 1rem .5rem 1rem; + background: var(--grey-1); +} + +[data-ff-wizard-label='accessible'], +[data-ff-wizard-label='current'] { + font-weight: 500; + color: var(--blue); +} + diff --git a/app/javascript/legacy/components/nonprofit-branding.js b/app/javascript/legacy/components/nonprofit-branding.js index c343d6f5..fb9f326f 100644 --- a/app/javascript/legacy/components/nonprofit-branding.js +++ b/app/javascript/legacy/components/nonprofit-branding.js @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later import nonprofitBranding from '../../../../javascripts/src/lib/nonprofitBranding'; -module.exports = nonprofitBranding(app.nonprofit.brand_color) +export default nonprofitBranding(app.nonprofit.brand_color) diff --git a/app/javascript/packs/page__bank_accounts__confirm.js b/app/javascript/packs/page__bank_accounts__confirm.js index 98a85309..7a8a575e 100644 --- a/app/javascript/packs/page__bank_accounts__confirm.js +++ b/app/javascript/packs/page__bank_accounts__confirm.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/bank_accounts/confirm/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__campaigns__index.js b/app/javascript/packs/page__campaigns__index.js index 372a5f2a..7d8d84b6 100644 --- a/app/javascript/packs/page__campaigns__index.js +++ b/app/javascript/packs/page__campaigns__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/campaigns/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__campaigns__peer_to_peer.js b/app/javascript/packs/page__campaigns__peer_to_peer.js index 4a393e8d..9c88a1be 100644 --- a/app/javascript/packs/page__campaigns__peer_to_peer.js +++ b/app/javascript/packs/page__campaigns__peer_to_peer.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/campaigns/peer_to_peer/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__campaigns__show.js b/app/javascript/packs/page__campaigns__show.js index 000293df..abff9f6e 100644 --- a/app/javascript/packs/page__campaigns__show.js +++ b/app/javascript/packs/page__campaigns__show.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/campaigns/show/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__campaigns__supporters__index.js b/app/javascript/packs/page__campaigns__supporters__index.js index 78cd75de..201c25c0 100644 --- a/app/javascript/packs/page__campaigns__supporters__index.js +++ b/app/javascript/packs/page__campaigns__supporters__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/campaigns/supporters/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__events__index.js b/app/javascript/packs/page__events__index.js index b1157e54..95ee1fd0 100644 --- a/app/javascript/packs/page__events__index.js +++ b/app/javascript/packs/page__events__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/events/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__events__show.js b/app/javascript/packs/page__events__show.js index c5979939..989c1498 100644 --- a/app/javascript/packs/page__events__show.js +++ b/app/javascript/packs/page__events__show.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/events/show/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__events__stats.js b/app/javascript/packs/page__events__stats.js index 506435f6..f3f17718 100644 --- a/app/javascript/packs/page__events__stats.js +++ b/app/javascript/packs/page__events__stats.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/events/stats/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__btn.js b/app/javascript/packs/page__nonprofits__btn.js index dfd0d65e..51cf0b41 100644 --- a/app/javascript/packs/page__nonprofits__btn.js +++ b/app/javascript/packs/page__nonprofits__btn.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/btn/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__button.js b/app/javascript/packs/page__nonprofits__button.js index 18e55f95..78f16c5b 100644 --- a/app/javascript/packs/page__nonprofits__button.js +++ b/app/javascript/packs/page__nonprofits__button.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/button/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__cards__edit.js b/app/javascript/packs/page__nonprofits__cards__edit.js index 7f52e9c1..e3be4ae0 100644 --- a/app/javascript/packs/page__nonprofits__cards__edit.js +++ b/app/javascript/packs/page__nonprofits__cards__edit.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/cards/edit/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__dashboard.js b/app/javascript/packs/page__nonprofits__dashboard.js index 0637951e..fc7ef4f1 100644 --- a/app/javascript/packs/page__nonprofits__dashboard.js +++ b/app/javascript/packs/page__nonprofits__dashboard.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/dashboard/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__donate.js b/app/javascript/packs/page__nonprofits__donate.js index be5a4333..b36bf4cc 100644 --- a/app/javascript/packs/page__nonprofits__donate.js +++ b/app/javascript/packs/page__nonprofits__donate.js @@ -1 +1,2 @@ +require('../legacy/page.js') require('../legacy/nonprofits/donate/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__edit.js b/app/javascript/packs/page__nonprofits__edit.js index b1e51cc1..aacb30d1 100644 --- a/app/javascript/packs/page__nonprofits__edit.js +++ b/app/javascript/packs/page__nonprofits__edit.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/edit/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__payments__index.js b/app/javascript/packs/page__nonprofits__payments__index.js index 8d9a7568..866269b8 100644 --- a/app/javascript/packs/page__nonprofits__payments__index.js +++ b/app/javascript/packs/page__nonprofits__payments__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/payments/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__payouts__index.js b/app/javascript/packs/page__nonprofits__payouts__index.js index 286e995c..5bbbd972 100644 --- a/app/javascript/packs/page__nonprofits__payouts__index.js +++ b/app/javascript/packs/page__nonprofits__payouts__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/payouts/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__recurring_donations__index.js b/app/javascript/packs/page__nonprofits__recurring_donations__index.js index 21bd60ee..8f60a736 100644 --- a/app/javascript/packs/page__nonprofits__recurring_donations__index.js +++ b/app/javascript/packs/page__nonprofits__recurring_donations__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/recurring_donations/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__show.js b/app/javascript/packs/page__nonprofits__show.js index fe6656fc..7035699f 100644 --- a/app/javascript/packs/page__nonprofits__show.js +++ b/app/javascript/packs/page__nonprofits__show.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/show/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__supporter_form.js b/app/javascript/packs/page__nonprofits__supporter_form.js index d479a5ad..94f8e768 100644 --- a/app/javascript/packs/page__nonprofits__supporter_form.js +++ b/app/javascript/packs/page__nonprofits__supporter_form.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/supporter_form/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__supporters__index.js b/app/javascript/packs/page__nonprofits__supporters__index.js index a68ed3cb..f408b7c8 100644 --- a/app/javascript/packs/page__nonprofits__supporters__index.js +++ b/app/javascript/packs/page__nonprofits__supporters__index.js @@ -1 +1,2 @@ +require('../legacy/page.js') require('../legacy/nonprofits/supporters/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__nonprofits__supporters__new.js b/app/javascript/packs/page__nonprofits__supporters__new.js index 75646822..693ff9c2 100644 --- a/app/javascript/packs/page__nonprofits__supporters__new.js +++ b/app/javascript/packs/page__nonprofits__supporters__new.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/nonprofits/supporters/new/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__recurring_donations__edit.js b/app/javascript/packs/page__recurring_donations__edit.js index 8a8c0a26..d77b9aa5 100644 --- a/app/javascript/packs/page__recurring_donations__edit.js +++ b/app/javascript/packs/page__recurring_donations__edit.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/recurring_donations/edit/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__settings__index.js b/app/javascript/packs/page__settings__index.js index 67694bf8..767aca08 100644 --- a/app/javascript/packs/page__settings__index.js +++ b/app/javascript/packs/page__settings__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/settings/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__super-admin.js b/app/javascript/packs/page__super-admin.js index afe2c607..fb362949 100644 --- a/app/javascript/packs/page__super-admin.js +++ b/app/javascript/packs/page__super-admin.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/super-admin/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__tickets__index.js b/app/javascript/packs/page__tickets__index.js index 53f2b897..5fe6f413 100644 --- a/app/javascript/packs/page__tickets__index.js +++ b/app/javascript/packs/page__tickets__index.js @@ -1 +1,3 @@ +require('../../../client/css/global/page.css') +require('../legacy/page.js') require('../legacy/tickets/index/page.js') \ No newline at end of file diff --git a/app/javascript/packs/registration_page.js b/app/javascript/packs/registration_page.js index 6294388d..59186633 100644 --- a/app/javascript/packs/registration_page.js +++ b/app/javascript/packs/registration_page.js @@ -1,5 +1,4 @@ // License: LGPL-3.0-or-later -require('../../../client/css/global/page.css') require('bootstrap-loader'); require('../../../javascripts/app/registration_page') diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index 38298350..14a817c1 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -65,10 +65,11 @@ class Nonprofit < ApplicationRecord has_many :campaigns, dependent: :destroy has_many :events, dependent: :destroy has_many :tickets, through: :events + has_many :roles, as: :host, dependent: :destroy has_many :users, through: :roles has_many :tag_masters, dependent: :destroy has_many :custom_field_masters, dependent: :destroy - has_many :roles, as: :host, dependent: :destroy + has_many :activities, as: :host, dependent: :destroy has_many :imports has_many :email_settings diff --git a/app/views/campaigns/index.html.erb b/app/views/campaigns/index.html.erb index 36a608a8..5e8c3fc7 100644 --- a/app/views/campaigns/index.html.erb +++ b/app/views/campaigns/index.html.erb @@ -6,10 +6,10 @@ <% end %> <%= content_for :javascripts do %> + <%= javascript_packs_with_chunks_tag 'page__', 'page__campaigns__index' %> - <%= javascript_packs_with_chunks_tag 'page__', 'page__campaigns__index' %> <% end %> <%= render 'components/header', diff --git a/app/views/campaigns/peer_to_peer.html.erb b/app/views/campaigns/peer_to_peer.html.erb index 6645d644..b7061ccc 100644 --- a/app/views/campaigns/peer_to_peer.html.erb +++ b/app/views/campaigns/peer_to_peer.html.erb @@ -35,7 +35,7 @@ }) <% end %> - <%= IncludeAsset.js '/client/js/campaigns/peer_to_peer/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__peer_to_peer' %> <% end %> <% if @parent_campaign && @parent_campaign.banner_image_url %> diff --git a/app/views/campaigns/show.html.erb b/app/views/campaigns/show.html.erb index 9c85d5d0..c8fb5511 100644 --- a/app/views/campaigns/show.html.erb +++ b/app/views/campaigns/show.html.erb @@ -25,7 +25,7 @@ <%= render 'schema', campaign: @campaign, url: @url %> <%= render 'common/froala' if current_campaign_editor? %> - <%= IncludeAsset.js '/client/js/campaigns/show/page.js' %> +<%= javascript_pack_tag 'i18n', 'page__campaigns' %> <% end %> <%= content_for :stylesheets do %> diff --git a/app/views/campaigns/supporters/index.html.erb b/app/views/campaigns/supporters/index.html.erb index bb74ce5f..9218b1f6 100644 --- a/app/views/campaigns/supporters/index.html.erb +++ b/app/views/campaigns/supporters/index.html.erb @@ -4,7 +4,7 @@ <%= content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/campaigns/supporters/index/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__campaigns__supporters' %> <% end %> <% content_for :head do %> diff --git a/app/views/devise/sessions/new.html.erb b/app/views/devise/sessions/new.html.erb index b38248a4..67924f1c 100755 --- a/app/views/devise/sessions/new.html.erb +++ b/app/views/devise/sessions/new.html.erb @@ -1,7 +1,7 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> <% content_for :title, t("login.header") %> <% content_for :javascripts do %> - <%= IncludeAsset.js '/app/session_login_pagex.js' %> + <%= javascript_pack_tag 'i18n', 'session_login_page' %> <% end %> <% content_for :stylesheets do %> <%= stylesheet_link_tag 'users/page' %> diff --git a/app/views/events/index.html.erb b/app/views/events/index.html.erb index 53ea81b2..0535f2fc 100644 --- a/app/views/events/index.html.erb +++ b/app/views/events/index.html.erb @@ -9,7 +9,7 @@ - <%= IncludeAsset.js '/client/js/events/index/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__events__index' %> <% end %> <%= render 'components/header', diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 094f52f6..40b8568f 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -31,7 +31,7 @@ <%= render 'common/froala' if current_event_editor? %> <%= render 'schema', event: @event, url: @url %> - <%= IncludeAsset.js '/client/js/events/show/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__events__show' %> <% end %> <%= content_for :head do %> diff --git a/app/views/events/stats.html.erb b/app/views/events/stats.html.erb index 2a49851f..025bb382 100644 --- a/app/views/events/stats.html.erb +++ b/app/views/events/stats.html.erb @@ -23,7 +23,7 @@ app.hide_activities = <%= @event.hide_activity_feed %> app.event_background_image = '<%= @event_background_image %>' - <%= IncludeAsset.js '/client/js/events/stats/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__events__stats' %> <% end %> <%= content_for :head do %> diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 30cb20d7..faecf981 100755 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -8,10 +8,6 @@ <%= render 'users/login_modal' %> <% end %> -
- -
- <%= render 'common/confirmation' %>
diff --git a/app/views/layouts/_javascripts.html.erb b/app/views/layouts/_javascripts.html.erb index f86da7d9..6767a3b3 100644 --- a/app/views/layouts/_javascripts.html.erb +++ b/app/views/layouts/_javascripts.html.erb @@ -6,7 +6,7 @@ <% if Settings.payment_provider.stripe_proprietary_v2_js %> <% else %> - <%= IncludeAsset.js "/client/js/stripe_wrapper/page.js" %> + <%= javascript_pack_tag "page__stripe_wrapper" %> <% end %> -<%= IncludeAsset.js '/client/js/i18n.js' %> - <%= yield :javascripts %> + diff --git a/app/views/layouts/page.html.erb b/app/views/layouts/page.html.erb index 9f31c083..1ed23f60 100644 --- a/app/views/layouts/page.html.erb +++ b/app/views/layouts/page.html.erb @@ -5,7 +5,7 @@ <%= render 'layouts/meta_tags' %> <%= yield :head %> - <%= IncludeAsset.css '/client/css/global/page.css' %> + <%= stylesheet_link_tag 'experiment/global/page' %> <%= yield :stylesheets %> diff --git a/app/views/nonprofits/bank_accounts/confirmation.html.erb b/app/views/nonprofits/bank_accounts/confirmation.html.erb index b9359cb2..e10457ce 100644 --- a/app/views/nonprofits/bank_accounts/confirmation.html.erb +++ b/app/views/nonprofits/bank_accounts/confirmation.html.erb @@ -8,7 +8,7 @@ } - <%= IncludeAsset.js '/client/js/bank_accounts/confirm/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__bank_accounts__confirm' %> <% end %>
diff --git a/app/views/nonprofits/btn.html.erb b/app/views/nonprofits/btn.html.erb index 25cfcf50..1ae29c4d 100644 --- a/app/views/nonprofits/btn.html.erb +++ b/app/views/nonprofits/btn.html.erb @@ -7,7 +7,7 @@ <% end %> <% content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/nonprofits/btn/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__btn' %> <% end %>
diff --git a/app/views/nonprofits/button/advanced.html.erb b/app/views/nonprofits/button/advanced.html.erb index 340e62cb..b40e6145 100644 --- a/app/views/nonprofits/button/advanced.html.erb +++ b/app/views/nonprofits/button/advanced.html.erb @@ -5,7 +5,7 @@ <% content_for(:footer_hidden) {'hidden'} %> <% content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/nonprofits/button/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__button' %> <% end %>
diff --git a/app/views/nonprofits/button/guided.html.erb b/app/views/nonprofits/button/guided.html.erb index 4559ed40..d939b0ce 100644 --- a/app/views/nonprofits/button/guided.html.erb +++ b/app/views/nonprofits/button/guided.html.erb @@ -5,7 +5,7 @@ <% content_for(:footer_hidden) {'hidden'} %> <% content_for :javascripts do %> -<%= IncludeAsset.js '/client/js/nonprofits/button/page.js' %> +<%= <%= javascript_pack_tag 'i18n', 'page__nonprofits__button' %> <% end %>
diff --git a/app/views/nonprofits/cards/edit.html.erb b/app/views/nonprofits/cards/edit.html.erb index 0969be8a..02bb3722 100644 --- a/app/views/nonprofits/cards/edit.html.erb +++ b/app/views/nonprofits/cards/edit.html.erb @@ -8,7 +8,7 @@ , subscription: <%= raw(@nonprofit.billing_subscription.to_json) %> } - <%= IncludeAsset.js '/client/js/nonprofits/cards/edit/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__cards__edit' %> <% end %>
diff --git a/app/views/nonprofits/dashboard.html.erb b/app/views/nonprofits/dashboard.html.erb index 9073e191..81e15761 100644 --- a/app/views/nonprofits/dashboard.html.erb +++ b/app/views/nonprofits/dashboard.html.erb @@ -4,10 +4,12 @@ <% end %> <%= content_for :javascripts do %> + + <%= javascript_pack_tag 'i18n', 'page__nonprofits__dashboard'%> - <%= IncludeAsset.js '/client/js/nonprofits/dashboard/page.js' %> + <% end %> <%= render 'components/header', diff --git a/app/views/nonprofits/payments/index.html.erb b/app/views/nonprofits/payments/index.html.erb index d9ea327f..76fb3a7d 100644 --- a/app/views/nonprofits/payments/index.html.erb +++ b/app/views/nonprofits/payments/index.html.erb @@ -4,20 +4,15 @@ <% content_for :stylesheets do %> <%= stylesheet_link_tag 'nonprofits/payments/index/page' %> - <%= IncludeAsset.css '/client/css/bootstrap.css' %> <% end %> <% content_for :javascripts do %> + + <%= javascript_pack_tag 'i18n', 'page__nonprofits__payments__index', 'edit_payment_pane' %> - <%= IncludeAsset.js '/client/js/nonprofits/payments/index/page.js' %> - <%= IncludeAsset.js '/app/react.js' %> - <%= IncludeAsset.js '/app/react-dom.js' %> - <%= IncludeAsset.js '/app/vendor.js' %> - <%= IncludeAsset.js '/app/edit_payment_panex.js' %> - diff --git a/app/views/nonprofits/recurring_donations/index.html.erb b/app/views/nonprofits/recurring_donations/index.html.erb index 686b11c6..86eb029a 100644 --- a/app/views/nonprofits/recurring_donations/index.html.erb +++ b/app/views/nonprofits/recurring_donations/index.html.erb @@ -6,7 +6,7 @@ <% content_for(:footer_hidden) {'hidden'} %> <%= content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/nonprofits/recurring_donations/index/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__recurring_donations__index' %> <% end %> <%= render 'nonprofits/transaction_title', diff --git a/app/views/nonprofits/show.html.erb b/app/views/nonprofits/show.html.erb index 00ea9ae4..27aab095 100755 --- a/app/views/nonprofits/show.html.erb +++ b/app/views/nonprofits/show.html.erb @@ -40,8 +40,7 @@ <%= render 'schema', npo: @nonprofit %> <%= render 'common/froala' if current_nonprofit_user? %> - <%= IncludeAsset.js '/client/js/nonprofits/show/page.js' %> - <%= IncludeAsset.js '/client/js/nonprofits/edit/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__show', 'page__nonprofits__edit' %> <% # needs to be after the page load because that's when I18n is included %> - <%= IncludeAsset.js '/client/js/recurring_donations/edit/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__recurring_donations__edit' %> <% end %> <% content_for :stylesheets do %> diff --git a/app/views/settings/index.html.erb b/app/views/settings/index.html.erb index 86579f3a..d45ea6bb 100644 --- a/app/views/settings/index.html.erb +++ b/app/views/settings/index.html.erb @@ -11,7 +11,7 @@ app.current_user_id = <%= @user.id %> <%= render 'common/froala' if current_nonprofit_user? %> - <%= IncludeAsset.js '/client/js/settings/index/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__settings__index' %> <% end %>
diff --git a/app/views/super_admins/index.html.erb b/app/views/super_admins/index.html.erb index d46b46b7..b9dcb8ba 100644 --- a/app/views/super_admins/index.html.erb +++ b/app/views/super_admins/index.html.erb @@ -1,6 +1,6 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> <%= content_for :javascripts do %> - <%= IncludeAsset.js '/client/js/super-admin/page.js' %> + <%= <%= javascript_pack_tag 'i18n', 'page__super-admin' %> <% end %> <% content_for(:hide_nav_beacon) {'true'} %> diff --git a/app/views/tickets/index.html.erb b/app/views/tickets/index.html.erb index e31ab164..bcf7ec8a 100644 --- a/app/views/tickets/index.html.erb +++ b/app/views/tickets/index.html.erb @@ -7,7 +7,7 @@ appl.def("event_name", '<%= @event.name %>') - <%= IncludeAsset.js '/client/js/tickets/index/page.js' %> + <%= javascript_pack_tag 'i18n', 'page__tickets__index' %> <% end %> <% content_for :head do %> diff --git a/config/commitchange.yml b/config/commitchange.yml index 15a12606..da02e6d3 100644 --- a/config/commitchange.yml +++ b/config/commitchange.yml @@ -34,7 +34,7 @@ payment_provider: stripe_proprietary_v2_js: true default_bp: - id: 40 + id: 1 percentage_fee: 0.020 devise: From 18d5aa9e6df2f7066cb4927f3e1939627fb2440a Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 10 Dec 2019 16:00:27 -0600 Subject: [PATCH 268/440] Fix database pool issue --- config/database.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/database.yml b/config/database.yml index 556f7a58..d6c3076f 100755 --- a/config/database.yml +++ b/config/database.yml @@ -17,7 +17,7 @@ development: adapter: postgresql encoding: unicode database: commitchange_development - pool: 5 + pool: 20 username: houdini_user password: password host: <%= ENV['DATABASE_HOST'] || 'localhost' %> From 1819a1f4b354fb00e989fc7f0bb0ceb7bade68a6 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 10 Dec 2019 16:00:36 -0600 Subject: [PATCH 269/440] Fix routing bugs --- app/assets/images/favicon.ico | Bin 0 -> 1150 bytes .../nonprofits/supporters_controller.rb | 6 +++--- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 app/assets/images/favicon.ico diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..31390449ac85b0629270931c81cc0d56179be8e4 GIT binary patch literal 1150 zcmeHGF%Ezr5UbIN$uPJO5=Wor!~=MZBM)HaYiAFms{@NBKzfvxE^q(?=RAXT#XbQG zfQ_I;< Date: Mon, 10 Feb 2020 16:50:10 -0600 Subject: [PATCH 270/440] Remove unneeded non-WOFF fonts --- app/assets/fonts/Bitter/Bitter-Bold.eot | Bin 22817 -> 0 bytes app/assets/fonts/Bitter/Bitter-Bold.svg | 248 --- app/assets/fonts/Bitter/Bitter-Bold.ttf | Bin 25896 -> 0 bytes app/assets/fonts/Bitter/Bitter-Regular.eot | Bin 23069 -> 0 bytes app/assets/fonts/Bitter/Bitter-Regular.svg | 274 --- app/assets/fonts/Bitter/Bitter-Regular.ttf | Bin 90628 -> 0 bytes app/assets/fonts/FontAwesome/FontAwesome.otf | Bin 75188 -> 0 bytes .../fonts/FontAwesome/fontawesome-webfont.eot | Bin 72449 -> 0 bytes .../fonts/FontAwesome/fontawesome-webfont.svg | 504 ----- .../fonts/FontAwesome/fontawesome-webfont.ttf | Bin 141564 -> 0 bytes .../fonts/Open_Sans/opensans-bold-webfont.eot | Bin 21811 -> 0 bytes .../fonts/Open_Sans/opensans-bold-webfont.svg | 1825 ----------------- .../fonts/Open_Sans/opensans-bold-webfont.ttf | Bin 46528 -> 0 bytes .../Open_Sans/opensans-light-webfont.eot | Bin 20550 -> 0 bytes .../Open_Sans/opensans-light-webfont.svg | 1825 ----------------- .../Open_Sans/opensans-light-webfont.ttf | Bin 44484 -> 0 bytes .../Open_Sans/opensans-regular-webfont.eot | Bin 21102 -> 0 bytes .../Open_Sans/opensans-regular-webfont.svg | 1825 ----------------- .../Open_Sans/opensans-regular-webfont.ttf | Bin 45112 -> 0 bytes .../opensans-condbold-webfont.eot | Bin 21980 -> 0 bytes .../opensans-condbold-webfont.svg | 1398 ------------- .../opensans-condbold-webfont.ttf | Bin 47336 -> 0 bytes .../fonts/Streamline/streamline-30px.eot | Bin 535350 -> 0 bytes .../fonts/Streamline/streamline-30px.svg | 1652 --------------- .../fonts/Streamline/streamline-30px.ttf | Bin 535160 -> 0 bytes .../stylesheets/boot/font-awesome.css.scss | 1571 -------------- .../stylesheets/boot/google-webfonts.css.scss | 79 - .../boot/streamline-icons.css.scss | 7 +- 28 files changed, 1 insertion(+), 11207 deletions(-) delete mode 100644 app/assets/fonts/Bitter/Bitter-Bold.eot delete mode 100644 app/assets/fonts/Bitter/Bitter-Bold.svg delete mode 100644 app/assets/fonts/Bitter/Bitter-Bold.ttf delete mode 100644 app/assets/fonts/Bitter/Bitter-Regular.eot delete mode 100644 app/assets/fonts/Bitter/Bitter-Regular.svg delete mode 100644 app/assets/fonts/Bitter/Bitter-Regular.ttf delete mode 100644 app/assets/fonts/FontAwesome/FontAwesome.otf delete mode 100755 app/assets/fonts/FontAwesome/fontawesome-webfont.eot delete mode 100755 app/assets/fonts/FontAwesome/fontawesome-webfont.svg delete mode 100755 app/assets/fonts/FontAwesome/fontawesome-webfont.ttf delete mode 100644 app/assets/fonts/Open_Sans/opensans-bold-webfont.eot delete mode 100644 app/assets/fonts/Open_Sans/opensans-bold-webfont.svg delete mode 100644 app/assets/fonts/Open_Sans/opensans-bold-webfont.ttf delete mode 100644 app/assets/fonts/Open_Sans/opensans-light-webfont.eot delete mode 100644 app/assets/fonts/Open_Sans/opensans-light-webfont.svg delete mode 100644 app/assets/fonts/Open_Sans/opensans-light-webfont.ttf delete mode 100644 app/assets/fonts/Open_Sans/opensans-regular-webfont.eot delete mode 100644 app/assets/fonts/Open_Sans/opensans-regular-webfont.svg delete mode 100644 app/assets/fonts/Open_Sans/opensans-regular-webfont.ttf delete mode 100644 app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.eot delete mode 100644 app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.svg delete mode 100644 app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.ttf delete mode 100644 app/assets/fonts/Streamline/streamline-30px.eot delete mode 100644 app/assets/fonts/Streamline/streamline-30px.svg delete mode 100644 app/assets/fonts/Streamline/streamline-30px.ttf delete mode 100644 app/assets/stylesheets/boot/font-awesome.css.scss delete mode 100644 app/assets/stylesheets/boot/google-webfonts.css.scss diff --git a/app/assets/fonts/Bitter/Bitter-Bold.eot b/app/assets/fonts/Bitter/Bitter-Bold.eot deleted file mode 100644 index 533502da59e18d1b4ef48155293f5479d62ffddb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 22817 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%0A4Y>6cAL8LLBidF5u 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Bitter/Bitter-Bold.ttf b/app/assets/fonts/Bitter/Bitter-Bold.ttf deleted file mode 100644 index 1390173b371f35b42718ddb5a005bbf3716d5511..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25896 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 RzTn@e^V-1=(jR|<{6D^iQQiOm diff --git a/app/assets/fonts/Bitter/Bitter-Regular.eot b/app/assets/fonts/Bitter/Bitter-Regular.eot deleted file mode 100644 index 46a2fc129589e075fc394a26c292647b187a8032..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 23069 zcmZU(Wm6ms)Fe8<0E08QGq}6EySuv++#$%|?(VL^T|$C81h?SsP6(3ZKD)QJ-rCyJ zA5K>v{Q*sC2mt69006N60SNeC`TqnAAO!~SpQfzd1OPx4Apr6JsQ@7Vi}S^X@BSa{ ze=;gS65s&v`VTe$kN;$JfaZV94&V)N0$BWSg$SVgziXcV^|=CE{#(WYUSc?XfvyGEsEzzxpo0nb}`G?!+xU#IuS95JaA}n01 z(qdJ=GE5irD49Hk)PsX|*)V1F`YJn^ijHp1r#7S1#nzuC=}EBfUxyoPttodB-El@* zQS-+YpNN+0BjZmzg;5MwbP@2xPY*CtPJ;Wqqzk0_^h|@Z8 zWq1z6ztmpQM}5h50KQV>cy`1rt+m$s;EN^Stv-)YL$d=4a8VpRULl_9T${sOx-D z*Y1c2bfxGv)ZXV-gFYu-`rSW?XMBxU`Bt3xPg`oj+KZ$bbVW)p(U=;2AEh1++1m7A z2W2^8&6o7{lpDC};$TbhiroQ{-%|*b>v0w$%J4Mw=%rzafK|ye`sV!~Zjhe-TzNkd zhaB*@Gp4i#(W7!wQ~X8I9v5`2Jxd}q`SsN?s!Ddu%246D9N|fXN-G_M@FdjmV;5W= zjh7*BB{JsXG^k_h6sjdFv=OB-*`Z+oZc@Z8G4Kf9{0Ltm!X`V8W*surq*fV_hYMDO z%ZXi%Z70RSfy7{iTH0qy&AE(BtPmc=Q6S*7&2uLSfMO|wXhX>oo0;~HPnATvYbBnc zlS{-P^IFXA7ig|OFnq~o)*Ag&z|so4s|(SGl_Bmz+CkO+{Oe;if(7|l`#Q^1GYmtW zA_DlFV0qX>l*)!(P(R>Qj+@jR4UY?humS_cfyN5ZNP@{HHIt~6fX+3i^E(1Lgm)y) z!0CLStasjvBDhPJMwU#M;l{}+e1PT782nz<(M(S^gs0Q*a##PQ3{|h zI=)dSuFo}*;SzWC5JxTn$9&3-D4;4f(6|K84Mt4OQK>CG@mqb%>N&r#u3E z=G8fn`iM*zdDHJbJv*mryg3K=x9nNi!lr7nlR_y*60{WS7e(9L@N3qhuZ#pQqdPvM z0S~V&!ik>$;>ptsB$7mBW}E0{wDctAwneOsvD*`RQK$mZv2*|rFY9B%#19sEE=1=c9U76} zzAGn&GH!?7^Q3HDLxzb_++go+uO0vSobzEl3Zj4JiX7?uVZ!o-|BHy>us#B|@H(?t zg+A92?M$gkn(nediX9p*)ZGQ2eqgQ#gATFQ-G9o}s8Ac1jP6d=8IlZ#4wBr(P4h<% zV=}RrB|_ya_&ea0+l0PauAa8qYxT=)<-XPku!z{&CdG+Nvzp^jo9zZS;j{<=FlaC9 zls%A!VIiE+kGmcy2p|YR5N=9-A2RwpPBC34#Hz^}`Ri|;hJ)Q@hf1~u>=9T{^r{4Y5Xtpr-j+mU zs=s19F7nfC}!-V5uMz$st#Et&%-Gcc_ z)OLCYL(hgdYbSi8F~2AE#^;uiv{<;$A3F+Q*I<;M0B5$-e^eAV=8BW|&ML^CnBid# zjM*NshBx8^&d|{r-e}nu-;GzJnqT3RfJGN&ZU(Ygn7C z?CFcRU!JuhjV2JYc+sQ!jel$*gi#w=EYf#M+!Tr;HoNpS?w_?zQ2~xcOD<#j58DXB z8d*qe8eoNs%bW_RYVwLRHy{3WNcXDr2L~O`NIIVhmy|o_-wCvpuqKW}LVXY#0!Smk z1C7+KYjyB3dYVT0tA+Y*D{q5?U42CfK+kIN@!PKwyCGkgcP$dgZ$VL9nrW9U@g-+Y z9AQ!dU5L1(QCOWdr#S@y17`WJg19%B+CgOE0NZK~azUk&#inTPTr=z>OIi5$%NPK( z*D|MGVmhkpuW<%Qds_n4hI4hjFxu_mPCvcd-z^496M{Lc?n@gFi+4}woqHBbK(in> zGQ7x+``yEf-{_EWMTZfk1fOlF1j@Q$a&L;W$$(gw;{6Q~$0LDj6o-p^OfwXfNd~Gs zmLS!EC5zWEsl$=NG^7T&d9s{R4xhS3<(=6v8tXnjY;+5F5I_fePnVy-w`p#l+sFBz7IR5y7N8>Gkx-L zm{Az*Kw*(MUZK83OeM^{5_e)CDJ1|u4S_74z<*2U9@*hCCm=l6AmhVydifT(XDkR4 z_w}ppBC=25imM2%NUiD@0f((I%!|6RsITEXdRY+{kT>QsJmGnzXGYx~f>>@$Tbw5w zBGb;CTefu5=sWwf3Zr&T3vv>-ZKjM&UJyjsU%q7%EFc^|D!HB740LJD`YC2c*wMml z43LHBt@+ut+xv-qG$+4z%>KZQ^OIQJK!dU{Z&t_FYG;Hl+ExPf_l~~zvyi$}wN_`2R;WB4c-zE) zYWRLAk!NA4LEm6t&S?+gD=%YZ>!ncJ;4ow=pL+Kt? zC>rv8N!EcSX^>(u$=)jBJvVl>a<2g@PVbT+41E01vZVbv8$Ku z`1@sy(sNRDywr?mCpI;J1@_sV8!ZA%U{@F?Q*7-d#h}l6Y!c_vO~*y48ZqweM>MFH zeOpU}Cl)o0IiP@b3U_!1qmsf~Z5AAc~sb3Kz z5!VM{#}+YU0L+9|8zTmF8>vT(8&$vo@r~{K%AB5wqW7a>d3e4PHq9KH%juZ>Hb_NR z;^cRvVfdD@=?c62y}k-J#Ds%mBxR`?e%XPvKr)Nm${A67ES*tMp}~JwGOxk}1Le)r z0#Xz_v*ReN!Jsf|pWp|E+G#SKsD z=xC4@azzwpGuArE!g_k(o1cH&dcn|3=9D2-2|qJ6Iz3Qy>!qnna}wp27XpXK9uh3 z1>P=SW<-3AKPOxgfV-$nP{ai*TjMaZWft+#m+O?cY$&5a-kh=azR`XY-9c3W-=V6A zJd^yCdS)4s&F+Jv1v50Mw$f(^L7h?+)`xHy%Q|KWbtM=$#V;@?)R=LrXsnf#=q4Ci zo5{pVD+srbV8p1B(t53SM}|Bk$(k`viU60nD^?bxxD%#1mc^Q3oIB&;(@99PaDrVy zO{VJ5ag%~je5iy1lGR*%lrOwVJ6QxTx?zz6NV8Z=O>x}M&!O|s26z;ygur`BD+u>j z`HP7rkX8*#OBm^o4Z7_$0j4egr`l-#^5YCPSb124U{i@+0FWc1D_K#N5kD38URFKl z*;kHr-YK%YJ}YHhvpN0}`%)NHnHS0SGkNCMiXm0ZteA6K+ti!a0=a&qrj5;1mcG^4 zN9*_*q2$;biVI}|o*c_LxyXXJgVG*?{x~;KpD8v_U-x6MuidyMwAK&9S?Y{pKkQ|a+kqU@ss8e`i)&86V>{xz&enVPNq~*Uq6j!J3x34rqXLYK7cg|<6>Gt@k zTptS`8`>gmNs7F-j47wV*PWfS?uc`d`3$N$(<@aMpgiTtJ03w8Ao62Y39%D6ZuG)q z)j$sqFs;Eg&KciExhPq}oaKVq(*x4Jo^diSWE0rwYUm^Q%tXioOrf8=LHev;A5Rdr2M=|yURp>NzRa5Lv`{;b$u8UJf>KKtF; zSLIH@@UJLWGIED271f|uwp*r2@SN(WAN=HLM+^EUfTH(y(u(^fU>ez$iO?Q!*QU$7GmP-m#UJK6HMn@+R<3GSs7sx`@$sHUX z(vmAY1TqPcx;z=|uW45=wawXB7lR8s5{uL9&A&|}hW|WL z!obT2%9V7iw|o-Kq3;rn-A*10kh2KA`=Z9oV2q4|x~##rLnoCc2Z3J|av|B|1z&Lq zzkDhd){M~hNV~tGt(7is`^2;po=I71bg3bigf(en#=6|p`d*XC=FQ&eYizp;6XoZ& z0$L-f74w7Q+#O=+E=|-TcE3=bMcHs52Dp0Gfm>+rtut102l~9H4oW2K@_b4I^xhW1x86NOfFpXj1aR1WaCN=fT)_S-WSW_9+I9hENZp<)t+9 zZydTQxMi;;{Y4t1$i$6tSy&~!c2()LGrO=UugXiFlNw=qv-K=!#f%#)T*{)MrwLM~ zwJ*H55s91FuCPP$suyFl6dNHNPvi8jzV|nz;*^|$52T~i$t1|0s5W`z^Hamr6p#vz zz^IMKP`=6~nhc9W|7CuMOVuR(Td9<-*6?$C3@7zYbZXgKuga|n4LH0LM3ne6X4!gT zwU``)f+Z^{`s3@=E}pX0a;x3!GDXiKEc4E2kRn_f&;7IGE+B5%Xi+ze!r6eLZX#-VL}RYz8CE(|%)Yru*9hzc+28c!5YT0+s^Tc_V}E@kG>bLTsbbYkaS+by<4%K6%! z&n_!$MI0qw#14vmiTxk}@IqxGu8CqGv7c{B0;(1;(9&r3^dOwhlh@F#xg>uqCLCWv zY4(GO6*x|L6deSL2p}EAGufs3F_DcmBnrB446!cRV%}`b8)ibwtF7T_-NAAvyR$qR z8gA(iU2q&>vWNN15@#N3Yxcpys>R=T3GugG`)2OtFqpk8QK!&_a|iX#o!_wszDd+N z(BDthI40j9zU)s>ogjh^O!d&)IHLX2u3N7Yws*bwI$q&Z&AX^;v<*iFl-;^C+I%tI z7_cag@+8lss+!G~i|`Sg;E{a!W9ehgqP|U12c{D^6hzYn!hcO1%nWt0tQhx|7Gzei zquta{PlaQ8$unLzFiKlkIIb7{=3!jl*awt!UaLaXyeWa}pS8HFfd!)$YR{Doonxlp z5)We!oi!&(R7}ng+?C!`!?+j~Nqgj`7)4h&0MEOylFj!Y(%uub9x`}wM63{k9rJJq zx3O0XmJ06{2a=2g-Om6D)DY&Zu!LHkp$0vPeh|BZ6{m>Zv=6-Xf_EJ+J1~3a@oGMG#ypc7QeRq*(v6}RwMeWw zYNo-?4-a2FTXM}=70U_!i^L4oi?8r_%J($m2i#mxM5DFOVjtN0;`ajUXF8E#a9 zq*ZgsL0MuT?2Pl$!!Lx}O^EYc+$(qo$~o;uhKNxwNV+{jAB)Aq;4SrZZFX!)nj9AF zP0M5aRXemKgyh&Pz#&4!;Z;%3QZ$>6B(LJV9K(eh3D}*rcrOvrg?{DY+S57P`Mch_ z5gMT;)Nbwp1`1Z0cvC7>UXs_R(rwcRYKieFf2+N#m3~&JUYk2C!ty^*4zfI27>0j< zIj~*?miqW=?6 zyqV4Hvr9+~dsH;ghj(I=QDiZZ^vowV8MJ6_k({j>WN61#Z4ERz2m1$VHcmcH-=RDI zA>zGicc@4SgQ))sjV|N~(x+X%47H)~t0KyG<3ILIDBn7}c9yjg`hdq)=0R600?`HO ze}~?BDr4{6tV8xg#Q}C6=K1`x6#?f%J%o!w#O!|6!U+}aDh>ONfV}zwcnw}Y*RoqJk%J9ouPE=k9mZK?No+QF;TNk#M{HnKtTI3h+k zEUl3)^X*otI@m*D^ebKKesfNBe2w(!b9-(K4`mc0Qqc$9P*t)ljQZl(uey8$+Yo@D5b_W9I?jqOL zd?Wf-*dCaD*4~m@aC+#oN9OSbHA~JocOokAulfYN@mG?EmFV+x&(~xx_lw7sIW0x#!lwAd;U={*?|tSJt=w|mj#XUqYK_nk&3NrJEGUt!c5bDIS z9k6zm5wvxquO~pS#gCs(eq|^3b$&e=AyD6-U?wo~raa?c{hCi8q6ITR+2kAd?r&Xi zY#HFO&jskT7}lT4_>s(xdoH?~GNoLjm~Pk?N!I@&kR@J!m{yoY89b z>tkc_{^~Th%M+KtrQZW+l>S&g!LEDp^$wZJ{wwdgmLYN{vE0POqnm? z6{ZsGx>7Xk!p+Vwzg@cNDJoPTJrf6}nRNUftta{vZdjNqj{7(RTvezi8;C}FsrOkC z%-v4vL2=@l^k43K8bsqY2iSMx8qBpcCku0HbwN>kZl_#}wf>v-0%aHsRs3{3v4 zYwIzaE{)k`oG8x@;ssr9g07QP?ufKwa>eYQxAB$#3{-vN_FAD4koz%yYv}*gzTcQ^ zHf)EVKT7W^>Zukbqvw>Hp$Pgd|J-dBV!t1{@JItQ#EXW7s6}2L8rr8`Ss-v*1}4vr zvq4rm_FpgkK~*5y{MuX}SFgmXAYgO~2gYzy?n{xkqh~kGu27PXjrh5{NWyH%S7lF| zDM%eVIj;ICse#(_z)K}N`rBpit`tZqTAGXKbK&)%m*+b+ zYNnc?4L!mrV$>-Wb~&c5zbF0>J)Lc)!`AF4>F1HktzTa0vPw z%c1emF5m|1r}pSy)5IY_6rXhORQvmYWH_`}6h9(&e{O1_(suos&@)wDT6s|aX7So? zdSUdi%0odOk#@UJ+o%4uDGZLcalKR%xMoTz&|+v;vpxs2^JiHs1)fF>VVq!Gt9+KJM;`}=!^sCKi=ck<{2YoKDuv3%vr)PS0-%QuGSU?B2%URc;Ft(*^=9RT1V6=`gf;qaOMxAAI zHQB?=a5MuwiH;g0Mwh^jFgWimxV`i!+^4hwkwRbqe)+bn)}CfnOLp;+!Z=eO@HC61 z#f(#8&Zw4_v-HUBP#_k$cWFE5-ky(xQzaiI%)l^7b^-s zkt#jSx@;p4RcL`z^ht`b=Y4uo4_o=o2uPIVRX)wMQgjmLo#A1UuW`}TD+`Rfx)_)8 z9rr861mG>p4Kj%NaXKdFB@H@F;I+)Y;V2Xt$Is2i8;Zm6vyFo)4AZ%xXa#x@whE1) zEt){88`)o2q*5Os`92Tg7bkXyZx=fs5T7yjTTE~705$EnB`mmrtZHGyKSKF4lP=S* z1gXd7gLbY8D;W6dZJSg#`G`<-hR>Z^I>f`~M^ton4==;Vm~1GmAS5*#1+c8`v6AW@ zE}TgK?M~4{3o%AmO%Rl?_IsG@{apR9l2R=ra(eP&Z8^vOuTxj-^3v-XL+6yi2})SP zR+!uBDwq9RUFI*@@nM6V1hw%7_*4nW*Ujr!4wwz+qC{!YP2Nmkumsp9yInTt{yq>Z zY`~MpX2g^1eI_nuYjHImbXbl3%U`RXCKA!3KV#@1VG+Z|po#aGG_4%yb1O6WQ^FWp zD$fC1n#$odA(M!1pG{rBA+A==rOBqK5Hg5Ky$$G>_&oMBQxaVz?A)0|P6%O8^~&(F zM%TtMog)aR0>l_QJr9KqAxNy?<(^4!G!NLm3UjuZ2`7)(EZJzH%Dp0}F2zVtM-s`J z69x9^c6%$s?0gLN@O2HBzZ_b}J~;e{G?kw6?Ek)^+x^8^WZ5LkT(j-wyM z+m3+isDy%OSckVS`2ziVy!FK=fOWv^YY|q(L)7=^K846G+m{Adg9S&LBoUqk9a;)m z#gykJRNkNP7O9rzv*rwW8(#?@%fcfTB6ZHktJ6>B_f%vWHTDjUJR8xo!;liE$W~Slynba!Jmo7}K8I=Tri92hwrQ6(keIJYGAqaC((kJ;$MAiDULl2rW&2 zgl7RM*xZ>`^5Out1H`2ZMaX+O&-c3g+Jl$SEQQOp#B8+g+UmF!xI*xl!WVp%5&SZM zymdm3@#(a{B2^zOCDiN7GWVyskL5tBk9M)3ZwkZ5Z?D+7B|b&vd-h?YS=_SDt=n|g z@;DinOQXX&eN8zE@|7K|erqdP<*#8THMji3a=!F1pMa^o9Iyn7*;&chzcOJbfp zi}@S)5*GZWrfvK*VXYj7vmjyD0``>7=NSsBAt&+e299$nV%}(n* zSf*n!uaz$WRKI}bemv$LvQquBk~_$5Znr$dyC^aAn+(8XB=!)7bOO|TaM(SL99JO*>xBu!{$=z$Q-bwyRjq6;Ng|dRT2HI zH6!ele>PKgJ;u>?#u3*XiY;R6eeWye+J(LcZ1TrBplQvuO|oI-a4+)Op8P5qvW z`1?5=9cADQ;W;G*wsR&9bXznGFhP!06n9-O6l-1?Vi^(s%iYHyv>tqvGAgQWoNEzUIuXZ!!*K zx@v!8iZ7f!CFy}*6K*S}Cxv>t&hv!6llyy($=8FLVqEM8mZju?KH{Gwj!p~ z15@8j7oOG3z!O&Av;FqYGmNJ5?tq_QWcK?1C<-G}Z5~amlLj~W>qGt`m^RpjY-^kJ zH?ahSs;%LNe`VRLdLMFu%kz7Y!@mG|^k{z2=}15Ot%DxAD~|8|!*aOU^rdx+9^@M> z>q4ME4%x`5j%rd~@E;;r8fCovkc&<(iv#(&B0{2;@COo8-<=5>G6MK|w+YN6 z8!Q47b_oTaaj7w!49n(V0Ih}-t*9#C5WZ6739E?Oviy%P>y+d?#}?TJd<#LiI6{g~ zNKVZUZ>ULL+P%6p+QCN*nC>%&tKNXNcp354UBt;PEkri+sEFjO6p1^6S0hsRQ$;W0 zn`#)tW|%s*d;VOvYK?Y}rxu}0-n+yuv|!n8A?Du(W6dQ$LH3)5mH61DWg8eO!uCcb z>|4*0cSb?AzxT4BUtvc3**aYkcwbI86R&HB1qXMpb@A$Nb#sYfK!6BM|S+fq4CyytSLug8IVH#Op=LS2FV;8a`Jq30*!j zkI5%iimtG+wVz7iI0r~@!1jvHDy94~8a-`m5|YRtCPRiwvX=7RSE1r|=6{dlvurTw zWCL*wq%ZnPd8i*HVxZAW1h&OkGpb3|HEu(hc*bSaYKd-tJ+8gDMn>igaXF}lDrvEh zd)ceS@wS!F6sev9Kgo3>n5$=O4L+u%WzPM=NS24?G>FzbCd4hZcd+9A&FXW?$H`0K zyChd7j}hJq7_VZAa3)NND6iLWYX8e-oE_*-FlhDlVEV=iAZz;>nF~HC^XUBmRBhPz z6GB3G0=iHnKNw$I=B+YGzX;#a1GjvEV>f2X=Dl_}@RV%Tb+2fHEk|9rjdu2(QV_S~ zw_-JrShB0w*hg97)gunM)Q554l_o+<6-L)#`h~ydqbGDfcX74iw?Kx1XGAOhI%J|| zU5mszYCQOpy0$C;bIeXW$qAPKI$=l1QoSu^h!>!x$1`PxO3CU)d0I9Y6&xQsLPSkrjS7S1eAl34XOGsvrDk z5*@f`-JB7aiIof)?eBP&=$8k5WUM&QUxa?7bZqI$54YGRPxRM#nS=l2noE6tR4|P5 z$Et{*^xibPJT6WWwMjd!Jh~+nt}tz);n~tX2U%_$^-F0~C2-1kvz*d2G{+6{Qftg* z*cUf=|I5zn>vd_^Wl(oWBZ}&kBsyvQ@=&)lI-`9t<`^Cbz$swunRI7>um3I;$0HFU z8+&|#Ycy6*FBq1$XP^yv)@{X#L@K*r@LQm5^>?Y-!C4ViY{g#MqK(V%fRr^tbOh!m z%+4h#Vg%qgFseBjEYE`OHOg%dW_X6*)dwz+v}6Cd zke8yoC(OJGuF!r~q&8owu0)F%55$sz3rd)aUg9J{bx89dMdDoSwk_%$n$!+FT*TmS zyPaY_K|<7-7rnUS+ytm_fF-9!>U^zS=xC4zZ|o>nZDZ;I$5pcZhw=%jgL-q%e13Be zx%sB3c$kV5y)aIcV(QwsX68(LVvic-M^(U7M`qx!I-;ybM6+Hl>aMsZ_%mrp8X;d% z45cs%4f2NlYtB-Frs#t2lCpv+t>H2)#K!R;6chIGKa&Q5jn7WrIV{Su5)SlXV_uU7 z5M4LWnEkHIdx72zdX*{`_hdHveUScl#`6PJpg*@gK)k&(>~O!x@H#uhSk67ai?nF@ z>z7Z5`iv7+dc_CfP6FBp8^^Nz9XK|M*m-YJ%qc%K%RYFxZK>4M~--D zqPJNJnv>IrFp|b_IF)+mhzo>11QBC)BVW#Q zHn_?7!&0>O8l&w>$>sv)JMz_Y4%k~V%k8m^&sr|4#}$MTTm`9n`6M?fM|nPO?%w}% z-lU=b&2nXkH>nJ>W$HB4iiAEjj8*mL2A+2Bsoz)l_Z%+rqh7%E!=Iq)_A3!E=Yh?G^}E9@m5}k zUi)V8QZ|o#fnQEPV05vu;2h>HT3O^Vfd*Nyo58gH&!KCKdg! zKnEJXS<>8!zD)8T)Rz!OVJ6L}I+Nap!l8d~3^Wvqx{3jQnd#D&>*(H8#tW$SMq!5Q zL%+9uxG}pwbt8{ma`-ZKqHw#c45N$r_Lc}PmBEt^t66b$TXjo)?idSjY z5@=<@WLabf0AS!?%mIvcEL=DFcnO1%+q0`~{4-&t%J!i8KVVWqX>eVPJA_jD3KkD7 zRY00k{m-|7vpPf}U6r4N`HhT#4woXF&%J?@VHbaf^33S(4AVq$iCn3y?4+eKT~ypF zJ-RY071K$y9(D7Ib6R*p8L7`vTwL0d^n3&G<#S9N=Lh1k4qA=Zi)C!vNYOh_KJO&A zUUpnLa4VA+25|LF1#+TV1onK0! zp3)MV?Qzf!!KCY<)>pFNX>#v0yOZ81+qGyiN&@<-r6 zp=t&oySZh?e+oG90solH%+N_&g}?B|TFyWQoLqnm8WXhJ-MpmV5|;syt6zQ?@y9CY zMC1?hgiJ(xhin2?VUef853LlNda`DfPgBZ%P)acG) zoII;oAZ7sr?XWwX!yWWnD(4*Ww7U{B>@5{kct?Uzd+sYR(arke>H+a$1S232L87VY zzQB&0b8Nk{T?h_*oQX*e0wBWBr=pR~Bdr8%ve!(|eC38sAUf#q`fdL=os~WJ@v#$= zF%*M#D9YRd$pn%YXV;iw$DG+oG)4CL^_?STyKHmEqA!>OVs^kBBO580uMcM!y^1<3 ze%bUkk%7vfrG{rn*h4~Dm9+ZCW6yytAHwR-?_{Yi?)O9B7X@#G-TjZkk-__aaK7Fb z=fY6Nf|w9^0L61+>+!a^4Xaf>{Fni)nK%_vl4rzTV#XZ$pKq-mdaa@H7sV1@V6r(TL`w)vQRt9m9S$8 z|L+TqOjBazlawXn0iBqZ6lFPJ!8`my0LxI5@#`<7FMe{Le{C_yCL0bg{+@pCf)V@+ zh%=}Yi6Np5vi^zEiXa+!8S`&O78E!lcOX+VoRl(X4yIlwiOefC3L$~NO*&#$a*tF# z0)fIF*2j2j9q5SeNRwa}r>O#4Jq#4C)mU}Wox>ve=?SO^fBrl>@R?^iyXt?R3l!F= z3ipcR&6yqc3}4zF9NjMWK1oRRaOA;rF@CDjw9@0?|q7cuQF;Mno>{~-xx&CrAL zyVJX@d{Yjr7&L+XjpM;X$oY$JopZW7?W?D_nPav!)j_7JKGS+5L@;r`e= zgx65poP0J9Q=Wj^;-hyO(k00-HJ$m;w+P-j7pwTz6SD^wTsZ@EoJe8qnJ zGuO1Wv;aHEITZ0D!dxX?qLUWVJvD(0f5^P)CT)=oU-A#C@r?#hvU~8W$uG6MBUS;i zsA58GVe%MVSZUek;#`&#owCmO)TLqOKNk(r-~-|DHyO~QFv_^*8y3V~u?!cxom}|> z+CrYxP}?RkK@^N*a87oU4hbdfa(a@nlv}^JW=T@~Sy`BjPn3~^L_}5F$+uy@towTtKC13UelZajz0}|N&fYo#x z05xjUTA!wj&0JtFZ7F!%&>X6RYra?}&EVr;5bo^VW&M{B!j^9tuYY+H%h0{A>iiwN zkup-rTb+7pE~{P*bX;!bPzxIWoe<-_(eoIUdoS@menF@1>_5O8ZH>T;-ijzH{si4J zVypP_owHX`)y)5zywv=O1+{6pHtttsxtk0(`luR-ec&a`lzV@*nedS?c9Nv`yduqh zJGY$n7vz*0im^V%7dqX9A=cB3;ft~$A6g=dp)E3uPd3Oyt?h*oS-?~SY)3{)r){Lz z(unzmj*3Oc#*O_NZ*kpy2|MyP7In(yDg5;GK;$zmHd}1{d33ow1tyzJ3%;39urV4+ zf+V`Hbd5{p@>JHmcHYi}=SOS9UyCg&U{3|Ifno!N2)|dFgLt$Oj#XeqiiDrat0<2V8*GN^J-p80_M>L?fi6hkD@Uq%jVp9%L|Y+yGyY>fpL@1o`zz zMpa853h9a<9@GvZI{X0?r1)|T<4-xNe1#j(TFJ41zj-k=3yKt(ql3Kqnr|7rhhx88 zL2wyJgoCV)@Kzubn@Ef;--fa{%{n-tk{&yLMhI&lGL^id!T*saT0T( zWZ{9sej$4(%iX%;C+NPEWJa!?QgA9Jdi$I*Ts&VGFNSyZs4JogN|et;fQNIy4Ew(N zXZcCOF^=dp!ZM|dNvhTy>w4e6=y)ytrzb{uoQPaok#(m8x?6*T!@gBixWL5M& z4*w{G(he2`+n-8=$tc%yLKR2WMEZor<>NNMzOB)}+z)pW{o}_Zz9TO6k>SG`pVDon z9rvW{zsc4N4>|XEnMA&jFNXZKa_Ep%z!HN+4KPzUC~bvDQ;8{!rhP$uyjTq1S_Fa3 zIis=(ZPm_;v=kz^>{+@)Y8au)CA55?UlhlscqH-9(iVj7nq8HXp^D5GR847q-ISzb zA8igbsSmtKF-3*R!F#W*-(a+k`E`eYzuxxW>w_i@5DGo~*%Q4%1F>q(1mKbc6{_?H z>BNW#Kba)@18szutrWX(F#Gkbj=nH8=HFWtP1dqf0;Tt-%MEKLmn`&=YG$EzTx!rj zYO5fG)3ZeC)(d&#!iFEDpG`TBTEZh)U>oi<_WuQ}8B^wDF?Lmq1(@jAcd2`r>lhqk zjP|)M;SuJ~eVB|TxWbur$xub4cI7hJQ<>T!-PTDdR>#qJW6b9W-wQBhBCQv67H_W5 z44fcZ*R)f3TAR_`l*X>!-{IK7bS$E-keg2CdsBJ}f7-HoMX_ z;1naxsau_s12HOxaAk;cN~%-%-8rSG0Z)Tbg@5*k)aS@pMzTXR2-Q~mtfz1cH}O{# zdE51q0~Q+hj|lt?0<}lPeu(DIrl}ZbG9Gut)uoRnj;vHSYS(ds&-T)t`R_bOpB4>> zBo+7)@V4R{WpFa($6!N7;}BLM9lGxhrzYeRso@Zp9IdG;%sFr^9_rM3PCsi__*ui{ zpc3+cs(>S!rMEEfm%1V~wc&++p%(#tEHy7E2RAHrHcsfLiOCF_-P7T}AkfC`+j_h_ zB#S#7pAVFybikp;(S>wiu6>`;O~vq;bAjAyV1a$opumJ`+|5|)%s+|Cj}yqJQ~p;2vvzvta*zs0kTg8hJ~tSgtE9B{Rib`B&tUt zW+sSCM0Z4Px}YPK!==|z91DC0)=+sh^fhOV!P->-cW0^?kt{`cWqVMtfjSo7xiBTi ziB0XZ+#>uJ)4gG01I%7`!@3a*zj2X_Si9jo#P!9Y;A`+tpYcISYyl^W0bt^+f}ma=Y`-D+hjU!2Ta(~v(aLXLU%uiKm&VKt)6}<@0c*EHsbbsHY!hQ z-X4_yk; zcx&X}c7M2cPR}6J?Pc}zD%c!#G{9QL1BPVK;f*pOp$6NRr-%qqlrUq&lX>yn z(5N(?ygxF8B=7y!5GM{eM$|jml#bZxrGFQ2(75OVUMb~J5X5G^a18ZVPG%bHj);$! zV*}~wZpep1+Po(`Qo@E(3_(yRE|FD92V@>v3srFq1yrgKGDe$kAm5RwV)o>I<``$H z5%Ea8G}s|?PU5F7uj~LqI)p-kJ`?=ocY`3SNaYK#`x4285(0dKsHRvup_YE zgiMYtN)kW`-QkyDQTyEz3!<(}!#q^N^G$%*SYH!#RG_!4qBbQdb3&G|l0-*FHTAH8wh`HNC2}lDC9u6@K%w1kXI%J_foK-Ov2$E7{)%8K1N}jD)$nw0YN@`z@9jcS5>1Wmz3^u)dgjV_Btb=p6OeiV=uw*t zAp1RRGypuN&;l%8*_n_!IJhO&pDKO6kc%yrION#(P1kVQR za2fWzPGdzokzz+Dc0iD7m=Qg(_;&BOGuFfu_%+x9_4lrq9j^0TvoKK`Pd!%)5GH^s z7xL^jx0f=ow>NFI;`{?+?(mX|jfh2TrCubogFq~M2rkso{gYfDBln`Z9N$AY+@it9 zULm5YJFutFSy?KoA}sR`C_Rx;Dsaxwe(F(Ld2JOTXay<&_eUkJ2=&o@QOa%GVtxwj z^ce*7nFW+&_KTZ}F8&X}eOKKUZOSq5R60n4gp30JVCB+&s0P0UL?Sohxl|;x-EY7# z80Al|p%Lt6)jXhofW{$Fc3^q`L#Lrv6%L2%)TsGM(qQDeKMP@uIFAIEU=r{SQx`=9 zD;yK66q5j_Z~%{zKm8u_K$B4?9p;U}3OIob)q$3_jJnmlHq>(i9sh8i4^lWbVy^OH zbBIbFEs1Ez4d|ON57L51H~`CTEkvx{i+mm-#UfZhdP1idK;=P!%xreJG`QcC3^mj> zs3H*oQxs2lyz7ftUE<=6N+B#P2xJGCtB9H*lM5uek(qH~Xt{v}gyz5~NB|#Ma+b>w z@B)_|{t!9eNT^7X+oZ>m!udL~D3&LL9MdUyfY<&sUh*7R_Z& z4Q-GR?-#*5iIt4{rk&p4UO+_A-`IBu`G`=pgh9NOv)!gTxey?-5}#)$Kqjpwa9UY8 zMJ!orzLp(eo8$YI!wI(tmn?WawwdY<8!TCnEx|4%|0_>Xg>B#v7Ks~JDdqI@T84^w zf2{Kq6EaOAwrEHfXI6FoTIAdaSVT)1)9i$!oRmrBM*RH1w2Kr|PxuXGWYDv4Zt8z` z^m{_hzz_^@N}jQ3-JNh+(wkVgxU=`DDwd-=jm9F}LJfujAdie2VG-D{4Ni}P9bx`u zLI>EU*j>fvr{T!i;vw!;Kqo8b%CIb@sG^LZA#}Vf4nGB+Lt>DVik*Mr*ctdk4F+ zQ&}%5Dzws5x|WT|CcWCm!CR;fkwXQSU5hIv+^~S>azu#Aec>xu$`oLsGJkUvNdV#% z5$6gq!?%j}gp8;+# zs)+@VhKhL^t+a52r%y#`$1i8l-tJ!{xmX{P|I7gv&9wqFm=5EPRN9({Pkd zXyY&lzR)mdyzvY&*o#urG+n5q)H0;eAdr|-3K?njVh+5%K%7RA$>tyO?nlC6 z3|95Y6}vPqZv^-rizT0Nu)#Z}L{9+@BPRj2y{aQ5_zq5mPSpVuPC(#e5dz4fO*F(n zGO*q>!-ycnMgxJCM%xP{;v%Q&)GJ}h=U!Y%7&`8;c_ezSZzM;|R?HVL?kPf0-Zp^6 zl%aFZitiZR(hbtr3hmtoD)x!dgf;~Fu?$(7dnUAY(wDofx|%io{W~Ze-Y}Bq5}PsI zsqRxOKgh=Is&NGf{-4pNDUl&p2e0@$`ldo~h;4fKfLPFFv}m0qv0 zim5kGEfqM;P)c1teN6d_Mn2;AV@4ZL7eMch&{P5_sIo(Ot^j-cD0e@s5lKh+=ge-M z!3>g;Y2~eI;-(AgyLDkpZ0ocVfs{!Hg4eK62XmY8cPo6BmBQ|kW%2~ zS^e>4b+EI1C17WARs*C&0!xw0DkX1psE|I(s7smG=XNYe4HN}suH+rrbx2N$0s@_1 za{3v@8oZ-drwJ+W4y*-`0^4iGlCdL1F9In z0-eHo{^x-mn_eV$q$;&%zc#%S#DzhOs!y&aQG%TN$g+}{M}7stE=L&5`_12M5>&yk zVBI%w;FsWIZiL3ztJvIY?3Qb$y$oF@kXT^;O+Z834OTGrN*Ui~OYqzp!IxPcaXI-g z@(KrhdKHlsNp`L9Qtkl~)E@#+;N=coldUa>>kxHk!M@z?7X z(nf?yP7ExZgsyI)Wr4s;kDd#h(coW)QM-bbz8hwCO~J7BKex2r1m-} z<+1$$IOvpgQ!LL#lqYdZITy+l+^+j=jhJwz>41pqaSCA)srBG@ZkOJ;Nz}E{GM0p zG7^4Mpebw_NFim-<-ySA+_S-qd>F?d<_LSK6a*e}fAB-WPRJBC*>$tTEecpjY9>@jP$4~`3R*DiTE=H11kXt+&$Z}4A47#cJdJHMTs`y#s)MXe1;h{V^ zot>oL2#-q=Z6-oRbO^8!dD6IZtSjJ9UqnV0&K)Q>5s{%dAtaM$Vo6NY({?QhkYK5A zaZsTjS|g|~(g=*YK@jm2RrLeEEK}FECh=*%;<7DJ~7Ea-1%?1a(Ob$ z#WaDy`Ad5r5KN&qzz~p`HwaRa**y+4rV6u>$PyE^i8nDz-BXS+P)TM+=E?VV+?^Sv?M38+5e7 z(bNP?uxYY(b`ND>gaNEGBZ^GQMG`|PnN?giMFjvH!UEHLNBm6A70S|jluFJ zyN{zj;Web+KPsMeyhu!^J@?YeV8yJdtr^jF$VI zNY=cl-Za9kc~%fB1RX*#dr7sJhGDo^5P55m{bL7*U$hu4r1biYn7H3i2XLY(2)dS-fF$+reIGQUEV#GE`J0)%EM#2C>Ij)>mD9AzxF zE@I$ABXui6vlC;#mIfqO=D1OsKt=Q=@JYZVG+8&k%5kqY`AHtVNA zw*Gry*zY|Qg;)lEJ+!U%dx6BF{>1l4}T0Z2}h?+ic; zT_|wiCOV0vaGfY5Nw5d|EyRRm+u+ePZV8rFtFjm{2$u^$NDw((;W|!-vfe|QQU5N7 zG)<`ynzayEMX|_BL>(lAcBDHBdn|Su3bJhx19C1vQ@DsGUCUOTlR%q{o1HL72Lj-! zP{K<~a~86^5d4%Bf<`V!d8{HX99!}na3EV?Y;;`EC@$qh#C_ohPZhbMO`dm*a(+G}4MUTQJlVm1h zVbKOIeYpPs0)zu-_K&OBTH#X`WxhVoTf$3ZeC%2jzPhw@^Ds)8cslwcgO%?K;t z1hGUoQHbq?O7<)mpbrwl2pL`ST!XxEv=A2u5ypm@;Y$;OXrJk2(_pCH5$6eoAaGNn z1c)OFCOoDi$!U=a1w+(WBC7_ymvW%c%>YKIC#>$pPDJi60yU(?fJUk!4leuY-?L#b zPv~CQBWyB0-9TL7OWJm);40h&mjnZK*-O;?+#rHPzW~t6S53o<(hO0fk(rkz)0KQ( zakktLsF|&f(GmRDTie+#5yTmSTUwSoaZ%hDhjk}?z+>_QVnRnF*-FvLh#KdJHw%4% zDSa&?dU9OY$~}I@Ex#+JHF`@Ybh1P~sJjbDpwM??U`G%i0PIC+A7ldDZnI&>Q>QMc zT9JX|yRd!9aH(}Biga@TiCsXVXEZ5ISX6d61Ps5nF+Dh)w=DJ4W==Bxx_ z%#6fB$H2f-!F=<=zDjArc6yLtaMDu017T`M1OgC{nNwlMNy?uNFjx!6rajS^8XEFX z39mdWbPh*ecBz&Kda>_`P}X6Cz)=bsI=`H6J!T0cnMHOw=#!||B=WUHaYN~vFujhZ z|17GPz+g!z;&7%_Y2&dL1xA%{Arm^k23H<}a5zAD?g?m_+3c5XOt{_}2Z)wyunJZJ z7@|s;Bf$h}jdfxJ^pdH?4^~G7H>cpkk8821ARFG zMy)sd6_i?giSDEj9SDEWrLhhmluip#=D|jQ4NSk^uhlf<=T%E!A!Y$J@K_AM zP-i5MI(X)9re(W~mJM+H{g1Q-f+;tAof%0Vd#sq7Z>&t_i&6Kkl~QGX@p)! zSx-zDwzzo8e@LYX$g5Jr6T+w!Z*|#PMYv-ttPx3JISx;;)He6Qt`r371*^Y>42jR7 zvkoY-q#lfhC(u{fLc;`{N@|wrF|HUj)RyYlT^&%jq%?#J2aC9tkj=7@cm0vQ#>^-e zxFY}3nm@+0GMzJXjYfZ#zJbS|42j)b5ub97YrnmsG z(UX)h=0)leZ1;LsGZl4~zVixmCSE==UOI4vg=hxdF)x)YId0@eZV`M*E|qi$Zm-F6 zAfR%=H>dQ1QrZ9xCXWPC$_zKcn>;PUe*q5~AsIcII;YL^)hIiwP0$sSs z_&XgxZSZFJ+#wYK)4!U6w{vEo|G%Tl`4eFSksU|-H~r&z8DB;QVw!Hc(67;RQZ<7 zk?>GQrRrA$9Z;~l1-g5=nJyJJXrO8ZEnmg#WJ;=-ms4mI GhBt7;hC59F diff --git a/app/assets/fonts/Bitter/Bitter-Regular.svg b/app/assets/fonts/Bitter/Bitter-Regular.svg deleted file mode 100644 index 8293c82f..00000000 --- a/app/assets/fonts/Bitter/Bitter-Regular.svg +++ /dev/null @@ -1,274 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Bitter/Bitter-Regular.ttf b/app/assets/fonts/Bitter/Bitter-Regular.ttf deleted file mode 100644 index 3b66905adfd60bb56d8abf34c21d6324f5b01222..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 90628 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�dk|cH2cdcz(yKi`S-`cj$4OcC`wPK)Z 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 EAF(mv_W%F@ diff --git a/app/assets/fonts/FontAwesome/FontAwesome.otf b/app/assets/fonts/FontAwesome/FontAwesome.otf deleted file mode 100644 index 3461e3fce6a37f2321ecbe64707f04c0a4f05424..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 75188 zcmd42cYG5^*C@QQyeoNEmI+v3OJ1!hp?BN#Bql&0F(rUQ=*C61jEjsU_uhM#yN!)a zZ=nSOfy5~U0x2Yzbn+xmdPp$|WF(Ia_sq&BJV=l2St!J@u6Ld}hLIigG(oNmSNVYp2bu*`8&mCT(g%JrKs|W6VjPjD7@?@<57`EsF_Gij(~7S;$jX z2uy5njLz_R|~-giuu0_{g+f+ve88OQ{Hz6>Y}qGm4HH8LdoEP_Ir~wHA13xKq37E z1Q7#%ImkKEQfdVC%s|@tAvjG9rGf|G%tLS)wVE;wz~z*JdUGJ{Lb24ffiy>{sLuw{ zN=i%p4&x(nc7ggcB(4K#2{l|&I*@jvl#*QoX(=^T^1?vc`5_#d8Y8(m0B0V8%cxE# z$pBnVc{p_qq+KX?r%B0{5Tf&5n`|=c zVocso$A%h=aRy_sSr<0ddtr36w}@);HtI||V*&u5GQ^q}ChAOv7#*33dEO5J<`I%J z*XfL=NJFf*@6;JnrxS?Jv(dU>lMZNv>x|xJgST0)^ZvUTCS9nR;D;OFCALM<4|cS0 zYNX*m0;fd-nOKu<8nuWrP;pc;Wuzjh2ue?xfq7<9)2SJhFQgVyVemeL(m{GHV42{( zj*5ZUn|hjxr9$DY5z3R_VDViTHB~GZO+`ceH&s%?2xUzWj8p>r63nNdWGs|hNF|Ez z3_x2)t$`3h#RG+4z;(3FM9l*V{~4dWakP0RwGPS}p_WLIvN!Z%D)eP4^k@*r2UcJ- zU?=@*H?{x2(58Ba^p5QH?|rs+TW>_~_TISOtlp~Lj^1Ov-M!a( zzv}J1P2C=H+Z$+4xIO*0|LtdQZ@V3LJL`7U?T*{WZg=0lcKa(r`~KGyb$|2y)%O?P zpZ?_!U)FuOW{B$$#SnIg%Mkex*jk1-50MUW8X_4&x!rfW>vqTOPq#nZZoBom{jU7N z?Kihy-F|Vq<@U4NPi{ZD{owY!+jnl?x_#sJZ|;=H59|LA1MdHSc=vyM#lTJ^gTau`vLeC!n{ysFfXP_Uc`a;;df9b8Q`%5CI-q;j?f_Z z08@+f2t13LIfyd|dpIWbJ7SE2M+X6Y`2Iv zkp55o6=8%9;E*zdF@cM1gm+?lAU^<05&JWMOK=9?GfrY#nxQ=#37!98@E7s2C_kX( zC)VL8>pEDTzy|wG(u4WIx(CZZyip8U549cAFn<07M;srB30*Ni03+$ax};f-cVgcG z?mU=>^dM|7CT$V}dFOaPnef&?TC8tyti(D1AN0WcgLMzq`5)sfN=5Jn`%Q2L%ZV|e zl|>C1nDg+#cYyEwFueh|8;M1@qnrlv{tx2;EpI}L@Bj%;S(HBnvCo4r5Z3J3VAh|L z<;C|91Fq}f+8ik7{a@>YGGgBWt|0H6vm9+D_>zG%!GU*vgSez_v3`gN?0**q@gSZe z&4DsfsLMf%#B&~$%c2BmvjBC70pNldvK)OGz|)9*7$^_8{)72JI)izrZzX|2bz&g6 z5X63xu^GT)2Fno{M$)8hgZ%>hi3CQMO{9n`r5)Xz4^*h=``X=^^&>Ji>7z6GQLVJL| z_aKddx*yOSg#T~iaf59p@jw_#VuBCxViH3?=0bWXsoR0$7|#Km$Kk!l!}JNx|I;6t zQ24u_Le9kh{ZB$TOd^pe9yTF>YR?YaZFd!x{0fp_1!PlmxoWQIAlbaCRI;O$No?ms&tQPAvhI zcLfzlZKi@i0oH?3y#tirbSj%Fr|PH{s)IU29j9KRy5UrMnfj3Wg8G5#rT(J&;SlaD zkxPb1R1%Fukc^g0koZa-mn@S6N`fTOk~m4Kq)<{PX_a(J4ogl*PD{>7UX{Elxh%OM z`M2a}$#0TBB}j75iFF$8q;VSOd9(^983P8*!UoeWONP6bYtPEAf-PDhGoA5e9%B|W&ob+o%}g+} zor!0%nS7>*zlX3A#CmdZBB z^s;1GuB<{3O7?^7SJ@vjyIdjH$RCx@kk69Omp?9FB3~hY zP97?ck{jen@yA4hRgPR6M?4L6qa z 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�HkOrW}z_vOChkv2rItMumE5S0hLwoSA%-~AW@ztW0A|C z2xQFs43$deYM>At)Fz$+w1rRa>wFV#?EmtOSDPyP?=AaOH~(GQghyxSxnb^bP%*KKHvsZf}z`w^*CiX33fjTbFKL)v!?6YHQD1s9L`%dCSIi zRrS%_f)S!Oj<&~NQs4jjJt+8Cy%NP!V)^59sa#_FjBIZq164ndJ2Q0B^bOOta<}G* zn#yK}MX)uTH8g zuPSxpZq5*oHIE&u3KPr229j?iAVjtv!(x{zlj9bLYbxtjS7w@_V)TA@t?eJI-~yb17}Ga-{v_{dt&$TJ^LH$YFk=r z_m&=zHeK;t{aDpv+5>=WAIP~M^~RH+F@MObkA;f*DZVfrPb?Y;8DFzPqHg=p-KZ zjAV#Z-j*ix$*QoZ%=M-!kE+t)ak)6Vz0v-Tb60+oz0oO!Ov?-%Q?{DzDgT}Vl)bGvO#!91nBx|GIyj94rCXHQC7&Kb5OZ`ip$ zBOxvxbF{Lj>Ei5VrtJX5t&n?%xl$a%*#!SOj46}p?QHiWuevnJa5~uF@K9X zE!|>H=WQcI#d)Qc3bQm+bS2c60jdrtVTYK8Qg}B~KnT970YZ(0FU9;3v;*rk!aUtY zN;mhMywNvT z5%vcoll&1TB18K^EGREsk>vYakw7I6-! zRu0&*p(5OOVi2ty;9`aP-X;q!YJmM!D&~@^9R+GKTr5unIuWGAA^VlW`q(KMv#UZ#s@RIS{zi7!dE<`@%l)3PFR(~SvW zv^k|=N0tG4!XReA%$Ewiz;RhbBS1PhL&wuNYI$kG(UUv5H|VRG5f)X-ysh)1R!QSO z`dEKmeFfQ07ibIv!Pa;Ol3~YJ-=x@;FuWnvx zSu%Ut>i)VR+*tJYhMGKm7g*vQs6i4$9)}ocG8~LaR6C<;)o%1R#hPtQWc|%cHzo%9 z+YkMPVxNj^BQpWk?jmo&nBRn+o&>r!mgbF_c;sX>x0=4f`WruH(b%mkrQV2Vkgo&Q z5a)Q8@MAaU3oDpQe!%+?xvKbj)TucD`#^Wapzcqf-wH9w$4ST=zy0*vAuIt?gf|sG z!n~y7Q5#;F{M>0^_{Cqmb?G|yJ1J7UBtAR;;OY^eiwsT}`x03iNncd~r?X-L@IjB! z3CGExt4A-KKfrzR_Feba4ykTea^pc&rMnVF zCaZzW1Na?qUHJi~g>walEet>?;>?l&8KOm|`2q4c2cE)V;oCpeL||h#lR4vXEw+#q zC&`>wp?hWTgpCvHO;pHrWQ1nu4URl1Cfqk7|0prMuID!mrM>uh5wSw-arPc$wCtO9(mQ^|F7z#RpdF57>U zKeG2+)4OUif5%7B9=Lo(O0iV) z;NR^6;99}H4hV^{k~)~2Q1V5E2muqA0~F}Nzb)88=FH3r>|D_A|6#{hg494Svvx1A+k%5Cr=xjU1RkZ=$6YY+7E5p0Stc znV0np82S$s+qa^+VGK+}5$)6wKnIv#1tu|%|~ zK4RDQ7HQW$#3g(kLEpUmaX7%Qlf_|+M-O`OS1P)?f+sKF;taz-=ygfoxJ({?;buUQ z_9l7w_R-7u8kH1=0mAzt=_UQ<;+Ma&WJD$T()%;{;z@GP*Iz-ud=N{%6jz6e{-Kb! z-8H}Z2^9fSH)YD;=dMnIzizdlirdH6e=_r-LAI{#3UZc?A{Z4Moi<_M^fkN{VU91v zmV%+V5BhQz@`Z3hvfH++u!cKdclzS@YVznY@C|r>Bl$wo2N8eQlX5*6ivncoKx`a` zZ0f@CfKBy7X&j>1&Pbb_G6Iyk!=d7Lk*mK0B_)W?{JV#)w{crabaa4V4)W3yBA3fv z>`{4)#hET{`|g?@Z>t+>iz+Jl{dJl9j0W6|ann+o*yXCnHJz+u!R? z`rsd=7ubVWkp<99h7B)hA%r{GAE&td#@;dUW22vWYWCBO&os{NZ2Ya}=9M!y9KQ-r zC|!H^RqxFNUTbe=)t0OXsqiknH(AD1ys)#Dq6shsv(YgAP!2F#{z(>LRoxh0 zd=dVhCeB`wQUxZJRsALdj0b>yUM+r=Tm~;~Dns_W{7*3zOx9<_i z?&5Mg($%@SW^*p@4;w@#3F}2RXnr_UwF#9moQ2+$#3!XEf=Ruq>;1g6g4AL|NOoay zt{c(~7SwPM2LDQzbsaaxd*W-dL(F!&xj=1`7!Vy@I_<70&qhf;e?td~S9;@BdUXW6qozGv-|UU^*lhD35I2BY;WZ84sSr ztbIaZ3n-upeTMl(WUF7Q_RIVa(XIYIFYsoQ#gwPoDRVgyd*)7`JALp=^-o`rCo~>) z3DL-Ip8rYx(@nfBQG4+A3%vdE^u|T!_#CGb+?5dsqk1D1tm?ub33TG~tE+3w)qi`M z!Su0Oz<~9-fSJj}RN3w6h9ma~-DOd=3((GZS$i&&V!BZ{Y>n*-SCCT@XbPSZkqGS9 z?dGyOTknPdSz!pd`IF`W+~;mr7m$BnQV+J~luGlY8Vpcy&Il0tl!l zj}Ha)W~Z#}kLYPTWlVHCyzi`?GuAHW664|!CY(BI?z09elF~%6fr#e*GWAwMZCrI? zjdbntogcKTz~)?yqAIU(au4w0tLfk9N`!{`A|6@{=&1vbVhrZ`r}qxOGKrJ_aAnf> zVTEDRsN|%`t*Tg;t}K~LNzT;8shbUQ%B=cko;T zf9~X-%ZT!4mGDg!<-1!{b(ys(%KOaL-2PKqnnJP+3%Xy*&5CQ6FUL8nd*g?XI0Wyl9QR2rOwQTsil!!@{!TR@A^#^Q{DRJYGHM?qGsCm+zvQ{1lU z*%Mw?-FV~ps25cq$^I0D#w!#Xks!Pn*T~85zBao5#xQkQ|AX(3=HsICwr*9?)spMc zr(Pbbl3)IlK63M9$ES@PSuJUGw3Z!&+Ut__fK?*XKKhR#B%EUc=iz_cAy~w__08$O zzru=#0f-02A^w;DAn#<+5f;=CrG#v6g!)-HS)_vHGhLQrG@Fgw zp}4h;E7Wx|@VRPhobF2AKLA?`10?#&n(SJ2H@<1b+UN~&y!?lJBlKw1&!UgLAo&1)J6<6Z~ULx(k#06#hG7K4gAPncC+Ub+u2FAq1#Zu$q;q4autF@Us|+vz%jmQrw=@MLNd(|JmsOqm4nB z-q2X6@78zVOl30FGMLl=J1&y7_jHa8)87N=EBPLz8UDMQgJkljdX2zo=xY6rE!(b0 z7N69|Zze8gAtN~!gy|b>*gF7L0VBzHu?SA~@M4F6vzsfE55};O%jC6s!PSh@ z3i7R3zNi-h%UaYtYlTw|^S>7><26SEHv=8|3PX>~VcxC0l-cEzWjzsqdQ#b%kn@xU|S#_5EGNI ze2WT0VOJEZ-YG^2GBN5KJ%xuDb)|X&1Kn?kmQe_y9*-OKW#BV_nsmu8V}`_U$y={$ zZB*x}F)J_Yvxu~|TbeC^ z*5bswFdZPSnY!6BQ9TuMV>}eFaQcBu2F?{KZCc30f)nd)m9*n$wIaB{ku+H3*RiMe zSD zW3Ztu)ImF#wh*3}xKPJl8QU0}0{kPihCv?fsBd|1uLLYc)EGp0NA_4J!xA?a7DnIy z@KtZT)1ybBs1c!!m0Jags7%lbU{_fg>n+V|-cD0@3s z_TLTSOAo#bwDo@~T8br;6*6VQ{W;u;Wjmm(hF|N%udx^2R{YRkGI%f;g;rp08Qj?0 zL0DXihHs^d0>b%V=(wI956_bRl;c^NJn&`6;=b-+ubxPs~haoDgsq0yE>Aw?4f@U zdcGrc-03ZVWT#lYdGqSkoA)0%vY-9ij11-m){x(YV@E@R>#sgit@m36`*H3bm^rOw zsDjGO<_9hmJjJN#1Kl4lQq$0GhZ8?C5R~YM#jp~BV-YsR7J<*;wa02;Dr;jg<*oMO z{NM+5xP`)zj+R#o*)StMpV>Zs+^kvS#}AVlzpv@)0Fe3a_#1-JAki+G7{a26!?<|;{VRIqdxBn&o&UL1BdFyy{N^Vk$DzP}Fs>JCma&o2B?q+wb z6cL<{ASxZk2;?vTa5iuqU3k{9%i5v%2CHV@Z@>-iOV87ml{Al|vFk>ivl&^X5xuWOQ{rl@<2k<}AzYVxThpQu|Cr_TNTHu;rq2-3qp!EP_ z4p7nFZu;oS0UTnm8bP5W6v;CDnb7it&Mnf>tgkT4sqNz zXH%)m;{ssg9mFSE_kPsUrp8TGvnP88)-Veht#~nP=~PhPUGzhd_s4HEKojf%Io)TuH34R&?jzjM@Z?&6Ceuo zA`{fm%6b;@pphqdf2J^fi0rS{$OQ!%xeVljUUUM$u0SvJq9d2_`iROcd-W~){gv*$ z(mz^${uoGS67=&cVJ*BnVt?XZcZ<7npMI|tt$qDxYw+u|i)3BF01|kEp^NGgv_shP z#$1gygn+yq80}M8XdVI6wv||`8Z=V^72dU9z;_4^m*IS9;WxA*2RaLD`QJ6&nwQWt zxO#TMR37RS!Hm%xkk#ZK1dA_e=zd6SCXxLRw2&RY@f{Bpqw(Jn*>ciXjSsOQ*$*&w zfCv(dmCJ<*tlQRMtD5|iXh_Bfm21W_tFE(LLv{b75IWJ-%CHl@7^}hBhf1jOuwGrA z|6?0(XDvJAz&736p3=x?^cu_T7kr2}PxLy4{c$z|+rWrGGeOn($-t|J*7Ou#r`a?} z^513H*{=g=@iZo9Jto~T=wsh*BD`165W5coG~h9TN<4&vnyQeY*gBj3Z658d-VjlU zx&b>%L~UWuo}@$7kPi-_H6U)_q>?r=x*WS%6wAl;tske-D=&f}-)kenQl!$^$8 zT3(i4rY1vvTr{6Zcu$VU=*gABO(d#yWW)#GAC=PACBmej7njlz6RGN;lq{9x)Hj$K z(RJi@=d;d}2k3kjE|gvaQ1N3jBq3#Co=|aYZvjC{gT~kHU-dd7_nWY0eGFssXsBdB zZ=s85ShZ-_4@zc4Ks#>P38oh3-{ju}i8ujI_fhj+Bz)sLrLdP9Dw6H9%atkiB!e;8 z|469scO^v9lgf?nB`eE}6-D-P;gL|$qe|~HUAzG~EzGl_qM4Z20dZKa@b?q?9f4+C zKjA6S6^|xU)(lKp64?a0&Z2=|^R8<Xi%+iU9o&+@oN_VLc2=F&)XyNEs-F3a&7nA z_YFi?A|gwQ6-VAmdvmS}g~ODA%3+OZ$t6i@izF#2Ejpd2asCy4^UHHH)7HMVCPe5( z{}}1d6RplJ+qp;WE-5H1<%LB=AE+{xRSGDwdQVxyk!*gXkVpPa{tyMAdr6@|bqY_t zad$$c)DuV`8Vn|2ePH)?1@BBx+wP1CiE`E_>}HxL9pdd(F}t!k7G1Ne>uNkTZr%$_ z1XNJ5HenSPPQ$8!xpiCQzb)wsJ{ou}5l(l7qb88fds__Tp}E*&smkz#q!%Yx(la0e zW>I$C28Hw`Y=KcU6D}O?-U%{qC)G6ts#!ra46&j+n4&B&>F*#+CtnJ4SkZ>764Zui z3=>X3yG(xcPi3>fWPYdb?<2qN$H|wn7{Sy-v(IkjO>|?w)pX9h<*AF3m&QotM}-{8 z*_K1^T~*1KI%R=vxUP$!3oO{WVfIwjpVi{8Zg_7&3)djId#a9%IDrcGIL^;)q)Xbp zZvZhzzHJw@?c`e+E78I%Qc*1<6V~05QJv6=gYOUz(#&SOHA&&uDtKZG{m;d{F8pugGBrE z_mC5DwpX3{5H|%ZVD}TY>w2QXCGEp^g_baCXd;6At zTb^8*UX_7%D@ZnNB#n1vbwv&8hPnV~uK}NARfxQgjsBkp;cpdMZ^?uqWI{Ew!7bi^ zx%r%I7FN=WsNT!9`-4%9p4q<$Pl$RkH=QMXQNhVI2dSQa3G-N@ytU_Z9s7K+W;eXB zefc|fZDB>Dw(N-7lFv7FxkOv?cSk{I)g|7yd)GdE~kz4Wq-B!G;1;IuNJpO{7chPaj3gRw3n2h$`y3C z_a&@Z*wI}GPkakKATKbhW4x~^-i}|jYnd9KZrYNvBNhw91J~A#S&OSyg{-Q6JL-fQ z+a1_f)>MZIiauA%!NRtLx1pRR#F1G~V)dA*=$wC-N;oRl4xXmovaP00$4BkSIr658 zmB_rwS&N+KZ3R**iu&Xrk0stoCF-eDn!W&hj`RleT}2)mdH$JxgHbF6ZQRvPQ*DXc z7OUf;wyn-ts~$7P?HS7(tn(l*D24rQWv{Acj`pFwBk9AcF`w+XdjEb^(QjP7+YJZPF3JBi^4e#rPj;X9 zoFfl6gXq&oO&&V`^=~|LSgs-(pNRzwL7;6iNg;~iX;9)BBt8_8WOV%Fa0K#5Ya~~P zfDJGPNHB~+#3vB;VfjbA1lks#;u(*r93nadp==}y>x6F&cielvqSU=}`Qy{6zD`7-e^{YvLqMq((z%Bz*l zb2!MwWb8eVeNg@9FCsW0L5{sk6v}6=zU*~3{G>gAK8b|z zGHJbofC&x0gNhL@U`=MhF>6*psA)>TAeR`^jo)NpM?2w@9 z_Oc?2qDo^>(t)m;`7B**G^9j&HttE08eK)EBCw~~8ez?c%IprZMUCq&rhV5xk73eZ z)z#W9-i$B>GNcbxBML6hYK$8JAtVSxi zEf5`5RHT$7AxF4y9JNv#w_yIf*XB<3|2;uzFdGmNXIxcfc7`>jNZ&q7nG+Dy$FiJn zdSK2-dXw?NTElu;yP+`pv%8c+ZJE&$h|P(LBh)6WS2J%_~X?*#Gw z-5K%ad_}xHGHVN+*43dT{{&c#yyPtq!NK(PBmERVYW&NE<;o*6fj#{syZ4N;Lq;#q z3rZ9&y3hfeq(c9mTd5Vne#Y z2G7KbeSbt9+`!iK!jI6%y~pev{MLVVA~k6EKYKx=-SD4o-MfcQv96#$~+;*E5nD3neY z&+mCZo!<5*(~Q%$*c|pdMleJ{D`1+Ak zU;G4cceduaQ)8~L=ICgNd@0oq!yF+G8>%7NF1460Z=_Sc8Acxa_CloAF>m6w*J2UHgwl#wJEH3yAl47nv_k+7lzU`4P|%Dq?t$hBOc2cbW{=*i zQ06S=ssKOor^pvej{Q{(fr#MM^rgSk!|%z+Ve&g3hXcNdaxrsXe z$;l37mA)9TZCyruf*v(VPqTUjq2eO?CV5jn+2`_IQTVU;uaGPBihzg6U)ME6(M^bC zchLjkL0#ROmb%yw##>~!MO~%h_;Je@%TK^(_=j@xWZK6v`P$@b7SiY1HI#H*+TnFo(z=Q6UyVuzp$J*?&E!7 z%I;8YFBx1+4NMIbe?asqOasvkETy{FHC-K>Fs>)zGW&xS^AX`gypT!hbOhi!Kt{3B z4X~`)CF$+6`Nk;RHMxx`l?e3)D`R&pv&2S0054dXl9(4=6bHI*OTgdvSI#N>5JGcsp^U)Yp{Mx}nk3-FP$1x{Qo0J#H+0ep+EB)*G#| zyTU2yKzmwoOk8T59^4#TbXg*kQ&e{AGBF`VWm#oGqrO&JqmL_%K`_R9UtGbe9AZdC z`EHS@-rgyJ*@^my5A$I6?6jF~RtlSi&40~K22vBo5*0-IF7ATz-oij(vSP^+5i6S^ zIfdMHsc=Yrh1q;gAsvbI%qkVJ zyboN^RV3FX4P~`;MO(0sk@fM`oh9rdYn92xm4<3{ zb!AaGB6Wy6l~h_A^wl9m-8Fv#@(b8q7_D$un;O#CpwueMGZKs9)$xgjp-x!+EAHX4DBma@+{eDjn{|7>C@v-jWJjf>xrw; zbJh9=Yh{Um@0I1nMU~YmsDkPc|9SuoexG)v#-|NfN?PPGekKLIeH%!U0@Z^Hv+P-* zoMz=o%~NB;)!=kL{V(z&r2L;0mwMc#HolGoSzV^0oWc-i4rr-r|7x-6ka@4ISt_K8 z%X&5uTU~iYInIzmh#F#(En+h01Jdt%GXCm^$@%JXV|h_Q`CDX!(pqSCcpqDoUh@x^Kv?yIfoJTh*12DVNZS|a%GXnjQXWgdkzTbz*wlM6Qa{&Xd+KzpCdsU`s8rg zPk#Ae)~Uh6WxjbI<4k@?oIjHlohU%%+eGdwNDkd_lfHhHqfecoect(9_5C-OfC?E* z9t*<{7Utk2S&Q-u9d_4EGC=7pHbL7pw*V2-hw18CDVd5+s*#>$m7-@z=*IN`#y&@1 zSJ48Z8%AbKgFp{iM_37%e`1^S(_x*pNT&f*(xPzlQj2= zkZ8g62L7om6Gu|qp4tf>3a*m z^erzf0n_eF^41TU-*8&qx<%faq`5EYE(;qeChrT^?L`)FLPLsj3=X}TE))ZX35$m= z^uLt2I&N+9nvj%(DTiaQp?Eh`z9uxN0J*PZGLsIwNSDJ#veTgwYLcv9XEv|niG{thOUzrh zTDBp^NVnvR;MKh(AtEbWK-HzH&5=-yIt+Nf#q5CesKu!E50E6}Wo%ocwpdM6WaYNe zSt9uwvQm6tzj?o!7E18bcCq=*R?8tZMUZqTDHIcT37K)ZmgH8DB`^Gw4%jD^SATQ# z-OtlWh?GY9-&COD>W!NI9Qj^ypWJ|$8$VTuR)APjUlq=g3zC15`bPR1&%7m<57EYL z;>?oFVnY#woGA@O8Ll*!6rw`;{|{mB0oT;^{}0njl3Qx6EnI~}?saeL-rKqmaiS=S zqOxW06-Y=3SwJ9z4Poz%2#R})imTSvx~ z+}xYoanAem9we0VST{eYn*J-X>TNT(@%jr+Y%!11)!RX`tp*y|nf)1m-oUyDb1+^2 zm?D&>{@>ODe2TtsG6fDs%YXh1xnb37K4bMEf1JJb&7Z7H_wlD_iIpOW7AiQYJcHp! zdlb(*Y5$`4!+R^Pb=3=dXz=cgm1JP?-kDhW-s*+@tPz&4p_vkz$vJ5}gZ@W*o^=(9 z=bmIG7oW33{kwxq;cv#-|76wMhHcnBo&)sp(5Fz{fG6M(W@UDEfC>*h!8-6N*WI!I zZ$m%>*l70t$;<7{Fj`>OW17RjdInmtrFR18{lk1mthVYhUZLr2L}0g2URvUR8}?C zfi8T`4**L5$YBH6U)y9@ixKYE?`A(AL*Z(!wpOn<6cZJvsxCa_-&dh^IK0rFazBvh zB-)a=RkYc|!{6W6-P>*d)+zW?FFD~S4+`?*_mMoG>Y#ch)u6l-c{PIYf@+#zFB&7r z=Y9+KaaFqDx%zr}`^n~x{L9Jr%1R6C^i76VRjUYm8*`5-P7paM)uLLezQuU{G>fjV zVVbAKNh&H3iX>-%12-AL@#+>o8kx-Pnye*OB5hbYkc=Uu&1 zkdX8f$3=@o8iNQt8{t?rXbgfveXJjiXY#Bw)`Qw*2wBgoH{=%=8)AJaUtZrAsF1|* zYrZot{)SonfQ#I=lk0IEHn36f+Qy^@BDsVcIOY zpIpZcQO2k6V^hRQae{DBem#8+*?r4vkyZ)|sj`^hY(If-a6)J_EsRbM^5A2V=aCth z6GMd-RwOhC>MOG=3TQ}@&rZ!H)S2p>V!qkM^bJ`rDu|2_$M{0so9C;KEjAkR>nWz% ze1^34YnrE;5jCGm*t*Ako0N#h_86$h_Hf~b$6RbrZ_C`%o^XMPdaQc+1TvwXs$Uj9 zqOva@Z@-X1u9vAH~e@1#p1B9>>(IE-2e<9grF|@EyS_O#aFkVY$RSxWW zL3wt)psqY2w2;n;&J2WIAI5`>tifa}FCaKHK2#7HuPcz!DOrkq7$J1tz-2sZON%&_ z1Uf<@HT3BO**SV+E{z&*n7KS=Jvu+qz5@BbMT1wrz4U$Tk9proUS>Rwdzo`f_A>4H zOEk#(ZdLDo)IlCiC<%#=m&V6a_zW*TH&>b~D9}$`Uy=W-4uS(NXlCj8to%E>|f9nK<}Aln(~4NEH)g#Ypkj% zs1r1lMSB_O%&1g9H$GH3(MvrUsESe~QHm%@j3|!q3G<49)D55f`Vcz0RNQi!k5=+d z>zWJ8sQU7T{7ykfL#%H(T@b4C+{qscMx8vj-0+&>?5t7^rPip68u>31xaPll$B}Lc zVF7`{(oPC_@j7!#s;aZY-6-tEGac%akcfcMeh9LpV!q#SVVRC7f*$fF6?TG@A9##{ z1hAz^j$5Ob5TV#o3D773hX&@U@aN^=npUvn+SdCLA`(u+O)BIh01enCl@U@}?rXa* z6n;;XAxkwldGPojVn&xiEGF>ZtGSAVE zWqn1g1Y*mBaO5(&)UNHvfp^| zwaG-Plxh&rG*iaJm{M;6mVm>#3_6ukv09j!0R7%tTO~LQD+M^i>?}=CCuE52#2E$% zVL}paLq3e{po_vd>ho26(=qU)*C&fKLIFtkVouOCdNVD+^23+!0YPEnV>s^$Y4b${?03?L<-a}ykB$%>Dl{5jk53i`$ zBw=(go5LiMXtV%aXqEa5JxC#cvxSXRQGna{%k)Zv1_pQ_jod(CVID|aa+z>&Smvv; zOPJB7om7E7E5Aa3HggTy6baZjM&XGgA&&$I$~I(#C^A|Ebq~O_q~b(*qCh0>$FvLy z#RHgDHV8=iF|DEmkSg4@Efg6dBY?X+42@Btss11_i1>BsooNQ542R+f6%Gf^5Eyt@dfwBt>;Nc<9)G70z&`WZEI7!gh*@q{V(4Qhf;ky5Eta}+%MGj75Y z-f*x5j}byTn3AVftCbuzk4a!N#O{r-d_kJoRlLL{OcFZJVUqB95>zu6&>P&$2+Ff7TbS z=E67A0IG3+c&1i0@L`6D6ynLI9UHRIWKQ2u<_ppTk2fvHjxFH#Va{i&$YppK^EWo% zN5eQc)wB*mh2}D2%s-%^PdH~y&|94pfY19lG&t%AbEHuN#+ z8yxIQeK^DQ4X&uCO(W1KUcy1?zI}YGLw@)W$^#LNA3B6|9ny}3HW-7dxP2pdxQVGI zVQ@`hK1e^8fy)~o0c;Al7e1JqF9(ZAJ_cQ66Ec&Y{{Eg6Ud8qFuW1H^iPv0H(+u~b zRq({$lg0~SOOOYNEM~pH7C_Ecs<_x<)-33Tz;hn9crgpXWXnh{vUu?VS+JVh3T6o! z3BMIKPn@mH$NF?+k3DmFSWkcanXY#{+{o}n4l^DCQ-G^D2F>A^3%O(P9L_W@v){Ci z+26O019-L$ss#8y%#SFP8Cg2Wbk4Mp{BH7@^#`e?-YRVUz~{F6AJ;{X2{gz^=Hek! zA%0_si2YBzm7yUiKb?F2s0j_fh~`c!z@rHy0o2aPX}V;A7*CAGQ`LB4;X?fko#UmJ zbGHmv^k{P(a#o_v0oO(M#7DOh=RXvY>iUNBdvgFk*=?Sy!Y9%0m?YcexV%Yulgg&p zou7n;_`QopyM0iA0j>M(-T63VTlp)29C~+y3}kfRlPc2K^(8T<*|+bAdgJ=0~0;d4r~rS z=$Ph^KI_rf&$Z9BH=l~06KDk^ApP5RX!m4dK{2M(SP-^Px?2)-sMGzL^r8e=B%qI! z?{h9@-pDxGK^*BzAR||2O%Log#z@>Lzn^_S6V1y*i<8kTxAU==!khQxN5}$G!U&ZAmL&)?d=xYY{!F(L zFNxU@;u5mUcfY%rySGcw<|qOOpk?Mp-kQuExn5L`SA|crU-RM4kTYPIaOO(VEy0xw z8QI`>kae;6dJTbOyj{zdg)SBB_s|=C>CoJmqNFlHT%L0N1)rfnvz=_VU~E~Js9uDu zN!Rdwv4@g~q(km9H^I)mnc0WvL*RKASQbK=VK;F*AE3 zM%sTE2*}9*xH?NF%7i*lI0(Uk_%Z@KEyH;UqJD%tF*QLeMhGx)s8oUw2(pcJ+VqSJ z3jN90X!WU3MzZ@iYbT?(lGJ1=vL$+q04Zm~QIgmON#JdkOyNJxHWIOdh36AB25B~2 zvf-U70atwj9sO*B7O*9_Sc5YLn~=zs3}7}eaUc*{^iLpklGz5ohe&9MWahuI8X!qC zHuz7yBwLZCvCGnA8nXGYTFWOu(qSv2uqy6p({S>Q6|k|BGbpnS`cte;NKKU4CCa3t z1U~FD9tkWTXaYpO1g*#*qd=Akh7d5ieQ%oqHC`Q`y?&42gut~j1bstpDcE_$?Kw>f_b>W&&p&eFwajvHD9Z70PM7wO1L zouYcdlc!++{gl4&to3o(_k?-y2unSgse}j@WriYCp~FP*eAnGy@BTVuu43Y`^!kjF z%*L$7(#FOkM`}8YP7sA%`p!0%=w({QRFJB^W#%(H)3Rxt5u8xVK6o@=F)icu`pqXl z(0!A*y)^!UQ)FfNfqm?o*-Nv=Ix2JkMZhmOPr<$Rz#vB4+e}8r78s5B1;*n1n8?W3 zmFF{^mz+&Z^Yy&ciyhsk|k zaW{3jUZ%d`s&Nzt7CLvn6Ba^Ia2JUhXf*Kp9`90D*JT>V_nsqfqv7eW^U`!XXvi$Lo9M5RZAxZRs#I<#O;Ld3%1~-tz=!2W2SGj zC_5@vA(zW?k_=IVwZ&A)G4`=vYfw#A|bhYTt)b;pRg_U9I51-c9f^c1u zG2(dq&5ADQPNt;9Kr^mI#OR*6xknAv;xidX32S17h5p-{XhMy4@AE%;l=qbnD*VO4>9@3b>QarqvHaB`H0aR1@S_n`iTcjqi*s@uiGq1q-oZ18JZTG< zw)YXH9#RgZ70Nop!Xilt2As4mRhE$gpk}f{QIN&@RK}!)63l$On@kfX#U-XjrSCm% zEP)6P3k&#mR#;d-RiDy0CFc(dztFhC_zXOfg&Hs%UIh*QVAc%JizUIU{~lfy|JjW;@j@?RnU&GxZ8x5Ud5k( z%XXtIM?hSfqJLEMUr0=a($1KBVtz8Z{R1+TEQ8Sm9^ zC9o6oj+Drg6iISIh2H~bZXLcuy|EgD2uHxH!T(PL85`ESF9t%qfQ2o@ zwE4FA4%Q$IN1v1cI7C7Mm@3o278)EKtvXy7Wyef0Pt{5cGK11inaa8G(>4*@nB)Y0s8JpmJeTFXNM=qxoy> zYpe4r3fSBHYfcBYvVGWWdOLjS5BO`LZdPubF8+?l0%L! zjaAHa4zlgw0T}U`6I@UoR|{UB<~gP|g%vC22lg{)72!V+l5|j&_}Yqssy|D+Z@~+!69^GaScsa52yQxzKK=cM z-LrEK-P?B)F?kgcrv#@eiwpAT_7e}=pKkwS{bBrRuouD1Tn|UN5kt$ z^7Jw=GN!;;I#ry4JEqyP3n%6UsS@Mt@OrGq9k0ddVntH2og^6!4L-ie!yQvtwvt?I zCl{;1lp1QBYG{Vl$r7a~O>CDY)=G79)G@^t7KZAfHkr~T17R{9b-aenz-l&VRHg?@ zON|zE_V9kFW12oipQ_8SgF?7o&xg%I9d9_+r$LSn+^7Di5b*h6#Yexv?FiVVgR#SG zyhFf}xcaGE?Pwd4qr&g$^`pF?D{Rd-^UxW5hT%0erDZgNbp8oxD3w)37e*HbX+lMW z@2cgoO9WX~(C9S!>MAR_eMdt)M3tyaTpFGiq$JF<;_-L*NAqkxnre?uq6$diC`DfY zZHh&JfPdl47D;rFL|)Y}9g0zbR?J;A739spZT$dL@I?E`_T3{mBA~nWS^zs-Hw7#Z zH`%iU&lNC^aA)rBfEqA3`l#Ms2WS?pyk~D$Z4afum9~9PxELltR_tAS#7=NfO~5uV zx&U1h3}G-)GOGbXDE8r~@L=%l4zm+wLnf?%&1?Z2(Gh%WRmc|ZZVupzf^1k(>oPPP z4OY=$leU|KX4&5Z-5>I_ZM|Lb8hrsn9FR(d18Wzp!DG-EsMAlfr+{$+eoG^GLAf9d z7WS%1m9TXVXwljfoQSlzB*5}wlOz$;Ui{hSF)Fb9$AK=1kvW8GP4>^=Ug=Z&d};~h zpBb)Mq#moDqjUuyyHM|3MWMVssl=a9l^sll{;lF4@$xqggzE0@vjz(3(m6<=c0`sczD#%cCfo?d5Ez2;jA0P>%u4uX9(xnt zMnHi1$Y0Pd!#8LfXQvYn<>M%89Q zGZn8{+m=}AU+J$skmw1WCb56MUyZm%T$it}Df>3=*|w_yB!T!4q8Wzcm0ftQFD&FG z3G>!^X}y}lnzV>piaz&w{MF;>#n{>#4}#eu?;_8}#Ky$tqO3;ZjnxS>!yiI)tk5um ztzlaR_>yoiTEaoObPsaf3!?cku=Q)OC9xUYrM~b*b^-tgzo@^2KBCQ%5gZ+fKWu`C zv>x+NKHk~`5^xUYQ;;(~3~uYk(9itdVYrlpovwl#@rTQlIR+3~FxNW=JORTfuq$HLkoY^4i_hWbDt_(rvY+$b5-LVDK9>(lq@Z?QUSR2CZF|Wg4Q;p^gOy&a>KX!D;WbavP zfDGw~4wy$nziK9!Y&0H4lP>_&sf0xP={QkIghoY&vT-;xYEZ+~${7sKPQeyLLcF~j zlR^O>#W=$99$XpUphj{8p6tNIoaf-WMKR^Lm@A^r(_G*3n`wn6hzenEMB86V$55CxBJp)^4B=cAm8dtyss-Oil-c`iC9-R_)O zDVTq4&E2JEkwx{{vg0LP>79fgS-9j*lAMKm$AUY@wU=~H6>Tn6Ug|wbdkJ7xhy0V4 z84iSra0obqZxz@Ncn)Z_fX3zuA7ff9&BPtE`stVza?o4hIlxCsYqa{R(aH=a_s~gV zY$22HCS%K5nSeGK=#Mu2N*2J<-~jlGJJZS$ngE#+SWRF)!qO^%$kYwg3Yg3CnEt=0 zxeSDM6CyarOco4eK>y19Ci{k=Q%fM{+V1XJeyw9S*h)}t+VzH&*~Eumxvgubm5v54 z2S-Qn8ui{!$E3xk3#ny_tqF1bxC;sQ6;z5)4g=)4zAxn zRCC$_EB3qcU4!=@45SuSb83r?bxr)H$hyPD)Q8`&kvzC^a`-qwAonM;2dD0;N=0A2 zR$E(3*Ve+}HRsJ6LIp|^8wyE|pnzr-Sc)>R)qngFe()Q*SH)G~fMzT?LV^Ft8x=j} z4cuDh9Ihp54}(LlK(#^TPkjZeWtEYfwL5A{)6=w zmc|PeDR5nx=e!fmE5(%cY&s0(AB7vv?uQIR|sT@4o(ho`RjT9V~)YT;yCb)^&9AA8x#~km_r| zx#Vbc9bJ9@7YNOPu@4%7XJ43c zcJ@W$K;`y}XZZcyZD4($Yu&el4dHHM{p0h{o5=$T-WQY^*u)(7 zDPg1VApQsr(L(<0cRk=F<VX>4jbXu>z8l{H zS-AFn#kbe_<}?UzjqRJoiM)ANbWiZ(lf23Yv?5IbvIv4%(%VC(ho*)E@s~!;^Ef`eHNsnChFyltOKAz*i4c@8GY;B*72(?XiyozI)(PG|AF@ z7_4e+n8VCraQI^%!0pTiCctc3da2)SnR?036y^`dpD7IA36OnE*vfz8wi&U%`pf*bX$0y{((NUW06%iufe~{liFeeB6gpSjhFg=K7!lt;t$TCQ@4VmAT$D5 zCOB1Dv8{$M)v6JnSU{KiH`5lf=YvN*s&A+<1769+?G#H&`=-Xg2qWv%TY(_QPM`4VEBFN)+;iy2NY%V4jX~ngdat z$@mjH&xqiId-(fGJQ~8Ot>!gKVB%&-?W6#AN~ z1iH}y&sG4>yuj9#0`+dcv+KNk*4dT%ue0kXrr*ddqU{IvQ{KLt!yu`)3fy8FI_RSf z18%WTS+`gkTQU(OxY2&)_0>pv{@1(SS|tQYSTov+gNz#k)_@i5;{hvL>!yFq zX#b#V&|)hvx19#&wzgbk{~QiGU)BB6`y;rE@4MWuy>GqCbceY~e(Q8~#R|ub%U5)7 zx_9r&<$Dx!0MR5ng|;^SJCR+8GOag|=!@~c<1e=EjPO1LvG>2>-50R84iU`9wy0+% zXPo-cfV)FkHlY$ACJFS+`jtiAzBW3P7xr{#`9j|$zWV$NkNkCM*xV0Y(@zJlz@Vve~T8= zPy@I88HnTa_(ck%yol;)>Ad?GvMfyzuUuMZ>(RqH{yU#5N&>8wgf%OoKXCwK*7bh}#-8-`-4?r00GyZ+ zR?5Mn7jDb1_oKzSI8{=TSgeZEiwXDkp2#->HOyD529JKzV{AW4>-DNUv3_7N0@tEo zvg=T*>u!%juC6TyPIR>%>7tlcW@q<;puAx7C z$%COuzb+RB+1!7WvTNwxFL{t^u@Ho@)L(t4$@G}im>52U+Ni<$_yV}usNtHrw;;7Z zQ$V>F1-75y_nbY~(^MZDQcV4F1M%C@+Pya8VXwlJ!iMC+v&H@4TJ)J{!O|bg8gYd&~>8a zNLQCf>u#3ha2HMa2~GM5O@{yI$3dp|CMR-pCFcyUJkl5%5*Zmn;X2;PS#w9?k7hqZ zA5nnYqR;pcB0HEHQWRNUZZrbg*vR|)$H#yF{o}k*6nJo?;7}5lo&%OG7PFqcr_krT zOF=)DbMWW95MyN7K$9=gWFG#kpCR-kq5#Qa9a(;WYuESx{QfYg_n7G#iH2`NE*Cgx z1P{ew7Z3ypA^2$*@X2DRJ`3J9w|(2UyLCVnFBj}mah^&JL?tmB<^!;EA`IiVq6CUz zc;`Ah&zJtZ5qmh zl)&yp`);VsLOLw?jZBz;hf?NnlXIUl16a+_w{Ou5CK2HW!kJk+|7>~rE-FF`*n#6RHqOm*)nEfpfHUP;U*BO+I~T|AfYGKnwQdrTtTyxoG~Hgn_>R;Il>U9vRs_5FW(C z-Um6Rp}Ys__ZrV#ZfYs-%sok@W@oFRpuPyiM&s~0b`{J2ZMgV=M`qUlH2eW=;+3>_ zWp@dne>ku-(~H)_r&7cmgMruYjZ!om5|%74LLowFOgV)Pdw;N>A=u~UMI>PR!?FmfKpI^W0Rl*nLBfUnaME&R-DY#_DBI|i#F%J)WMQnal3Dp% z_Bl!?$`lzS#O_;w%cNcqJVsXE^>oq(kTw-6QukoX)iOUKB|z=t&yQEdrN&Xxl2Un@ z;KsFL1zeZz7T=ZMQ`Xta(5HU63vQ*P%QPvPjC5OlDhFN0mVMSBJ#z+6n^VZX16kY{ z|6<{-HQ=jl1<2c2AU|b`iAyMv%+wTSXF=p-{=q6)J=IdwrEb?F@p-faS^tefjYo?* zQ@f-|wiEHHI5)8Dw!wqu;bG2r*lxg#BGjC3d5KC_dHf~cQo9Xb?^`p~dw*L!*++mWO~{97WGSA>k+vO4IU;!cFt6%6x*>K0n&~HSz%%{4 zooOsF3CPFT_wI2H+?qZ@YdFkqb2%yGI;q#jIY~3!ZEG}WML{_q!s?V7%Isz$$V`x$ zCJ4eKMT&4bBbI}XntREqytXXyBaZ1exV4aKo|5h7TPjkN^3+t0T0^SU>_GYH2+M@i z>>-i9n@p9ai4zkEiDc=H=}9vrcxdvOI^ifmsTp0gN!BYbQTwNEdT)XUCPbeRp=rR~= z*|~{{vu6k3p;ee;HzRWW!pVHheKj3%etp>U%19y0AoO|l^Jmr7wg{GGhqN5XMp{PB zd5S-)!Gl7GU*hK98!Nz^>C-WHQqRIyQM3>Kq!JH*HKRJf7P|WIzQAIg!?tDzxA1Ce zV3li^>W8i_yRNPWsxH^8L|a(%RF0|fLo4}i*^>!5R3)`!7A>pXs3UO&gFGC{k^jDZO zX6~{HFia-u@vy)dGXeu(_3#7R$AL|Sr|b^08&h39ux)k&Zm%+E1@`|K(12~MP-n0O6O*jW$O!5(>0m$d;&ZKNOC!LLi7PJ90)MqB75_A zrVU8Q`H;_Jjy@s1;G*t*Z6E%4Tn+MP>fZVzhrV$f^B=ttBy$Vx0z(iU&K-eM$@W$L zv4PQk@pi5dkP(N!5RAuPwsp*;GZOYzB?-h5iAV~h0vWyOG1{^5Z^2`3Rb56#6%A-= z>kp?|9c<6Q5sBJ+?*oVS7>Ak1%TCgYlKMWzhZR!Fd=>&=%r?Avft#Y_%4CS@OqMDX zN|{nle7MR}s+CHBATwoJfgU^~WSMz1FkMYKSP7*XHifYbYIfbM#CH&C1D|1R6ARFk z&c639cvG_8lk-U+z@7I0#;XZ>_;3I>d9AG3Dd?NSx_Vngv663|l}X2Y?eb{(QK~Qe zL&zK$%J$j(`xjiBpj*fe7FqLefX(0<2S{1}kApo_>sG*cx7)tR3W6H@?&3BQEwkDJ ziO4q{c{{;w&Jtc)KUr?|Kd)fs|MkN9_x_hQFevU%+hGAP5JGnUZ?h9H)3%%n2p!mu zkzh4<7hYJ~BHf3(|9?$ja0k>hwz3J>Wm#`9+0?AMwH-PMdD{X0F?tu0h2Pb#U-~v_N46%rIkb7l}qrM)3J!aw~Yh zS>lnuVt2Suk{1Bm0G#7zGbCA#18?}h}wc#v$$t0Q_x`#Z1H({PC^`gtQ|382ybEG?E~1u;2d`I$uDQ*oy2RGkgAI z=u?fvmKU%cBEd@&5a2nGuDu(Nn|yLScm~aZ(eo}e?iZIo@JBSz4!c>SC+A)%$iJcH zPzIF-D2M?%qQiTi;o)e=9%^6T6^@Uhf;DV%<~CGRH7MwPrYjsYO?^Xo(1FY*`!=CWQ~xi{gldC=JmY(OcLCMs3|Nsy7@rm|h>w@c zgmj-XD9(4s`0m-k2b-L|5fvI`ASyR%z>(YJ4AtdPUjOCfv;!Xd)n#0>uvF~Uk z3*(1#(5Lokz(Gk(wM$i`D$|tgL|d51IcluvIL-$oNQJ7Iod`I_sQMvFhZ+lS!3fKG z=xdv`XebY5nU|61%d0E#X9jQ_<)Xpb2lvomL7w>`TpE@=MdNAHc&<8EotFm!EXePS zfbMk-WMX>)Pqhh@bS`kF2cl8GB2TdT4B;*fn{;rwU_|eg6Qs2fy+voC4~~Z1-vvj9 z_4GrK&5pa6dJ>95**xo!Z+{HJ#K<-76KV5VCIgBoHVm-ZV}L3AF`Uw~R>0vo{V0t% zXD&VjXDI6}AU@xG8T+$JG#fzS8*l@_p({@^H^A62+u5p5Y#u~{S?MR9c;*tXU2A;l zH$lf%P2|Ht$X|!YQ06a9m0a`(zD-^^T5;pa82LOWZ=WxH@L*5favJt6 zfNT2pFnR29`Ed|2QD8E3VD=_#53Cs|x?2MOel0))pThCC5=h;66kY-v3?+~_rgLW; z-|>pF9w}x%CRgmY&V9($saBmG!3%r|m0poaOFmqG zW8p%F^$QnXcX;^l#`TBP2$S+RQpwXK2-Adu^*fv$_Ehda8V#K&6|XylqY#M{BEkH1 zJx?C@^gKRy%71qS-TT^{PtISrZvHlxniC;ZeAAJzHo=WEXKuV$V_aKFr|Pp(4T8r# zYvLBz=g4_IlLeUj)mN+|*iX|Cx2efcSWBBq0hFFg!QU@__}f6E z0<@EZrm^@e02u67R$pT}Q}w76Z7KmnTDVZpFl{7V4jkOI>pfzz28YIP=kC=X;DUU@O?dCj@aM+%#TaC<%;%`rLP@4#aBh>KLJ zR7xpp_tvQpy%e%O?y%cfG0*)xQ*A=%kwy}-%k=mk&$3DIs&VpK$OnBeQJ7jECVaN*A7 zQ~S4O(~J;G`&mWBnPus0ItXY(r*nQ*-@pGx@bh;|rvS3bEFEcY%p1XdGTd~Z8AO^_ z=Pk=p@dIYL;E!9sD%{`hdMbrk5NMlPoXc2p%)^;Sqv%*UDsN#L4;CTebHLeo1L<&c@O z1z}EKdG8F;@#fdROwhS%HpT_*2Zk17TbPJ$;msTr$b53_3aE?5;7eq*2uc@V6d7m! zsD8DI|6>^%Ty*Af`H{S~)8JWSB%%}+GUIcSe|a{=9-08e86dBPmz${i)^H}L%`g6pYztNMzH zx@(df@@|?r$a>R@d%p+rbT3BVlR*(tp$U-Nok-olgD~BQCkPfS6vZv2<%w~!BtbxQ zX<2PyZi$9A{`@-oR{@MQCwne~+aefTh=TZU&|%UqB-l4jl$jx-zQJGMl_&5v!Q}bE z=rwdoyd*^^@C&Y}tII2{)zPI-pBJ#+t!`-N?ipwrT^6s84(7ulrzQ{h26)Da)3rkC z5kR^Ao!CP#dbBurGA$PgW$}W8{}~k`B0xSd0uSRZb8=b+BA#KqWmlZ;gM{a2?)@uQ?n6M-&%FCi zSC&ydBYIV&m%qOksD6fVz5HvM`sX#()HL-k-Bjc6MfYXFwdqyfgY=%1U$Cn!a_;QT zQ}IhO$_-&<&~?CmH}P91i4r2=mB`2JXxq034nsVOC)P^z{cF){wR)P7^32tE9EZv3 zoe6h}VO;3r!Zr6i@9Nq6$9`;>hql=wA@>^QYaw<>n`(!`>rkK)?caY*cs2G?B*CoU z=_Oh*_2JD2u+-CD28WRnJZW(N=4IoL?b4zPMFr^udtYxkcJ>q!oM?Xe*R4UOGC+V5 zlwxs$AW;nFB(xAE=P*T_Y=a>yTQ5pbQ+ehh4ywaGq!L{C<6vfU863Svr`1rJGz~-&X%w2lW3@8KM&@UkA8|gkA(GT6<>@kk z3X$6s<}>a-M%K$DiPQPV{biFF+vg2ez25y(^vo@OA>sPZeVxN)Uu}e!yPL;tY0$&*| z1%-<;iIDRW`-W!VPcon)7pu+@<_q$3a`K@u+dPib`%57S_952D8qe_Xk_de%qzwlr zl)X4d;OLYX;Y>$Jk|nYH55sveISKg+iXCN@L%y3eu#aa@K-huA5*_Id#*UWmFfM>U z;9zTBf`36ntZ4AL9EHH!6WLlpv<2%YISOX*%uD(^PfY%$jN<`~{bMHoR`$XA>PRc= z)yy2O*#M~Jmg6o@eE!P1slKGXO6XO<5VW>r-8<7LHk(?VmQgD&PRt>!uj(aON&G(tTZ|0#z-W&XP5o%>BSntTR;xvg258be1nb=Jpl`hiA z`lG9rrHQsO9dN$|m%e|BR{u%AZ$jg15S>fxfBZ{ex8QVcQ9&AnQm9oFh&h37Z72o8 zm!4<~ou^$d@b*y14$%GuK?R{j0QZ>hnx~Vw^~bK?yWlw9-911^Fr#~?0urW9QY#g5C78PazNbi(3l*`71Vy|&4%Q$!r#h~$1Z@Hy+Bb>Jt;ns+tt1+6 zS6(Z;T)*n*<}PX%q&ph`*U(r1j03 za*f_dA2AlSpW~m7YV$TyS&_jRVEa258~fkIq|@6E?|%pQ6z-DvF6#C(O6Vbwkp6F;6C0IoU~`jxfj$h@{bFz0g0e|`4PE;OzxZGS z_{IOY!$A7U0K{S5g5+d=az0!j(RF;?7BcH&Gh#sl#gFW>0eUwyK7P^HsNRpAYo$*%r9E|5!8uSmFq8br(+hoBbb(^~k;| z>_tLCqFkH=d9I6X@o=zvfy7dH{*O`QfK4RR#>VGDAec^>7e42){;u% z+JlB)-|^r1{JP-*6<(R%T3J$2+?G-tL_8YT34=ddKDKqksnGJ&wzAUl(l%vj2yt$6&9a63h5pO7Z>ACwVx_SH z_TU}Z4cLkQ?gn`OLE62|QRwBdSs^xgR*Vhr4IEpHNI|MoQq(B|^fg$L72rb7Odet% zKokiknE&kC-GBD&pSAxR^)v_83RKQB5|JuSE3!lI)F3R9=NRlxHs=`9ibOefkZ_s; zL9!`^9C?w@V7IGTXo!OTmz}8{pMkdMj3_56O(?S4M zyOW+dqM|fI4y+uV(Hc~F5`$fzh4}z^Rh%!+H>4HVA84n$~yuXq3`ycX*Vf8a3IA?00FG?z-CN~zBqpE=C$^%_|x?mi{kBX4R&wQkXNY07CHce@GnYTUEA*inFIKo=f{&w=T|+7OU#tX_nN;YoZP3Saed z6rMn7xT#zFW{7P6ZRXX^rNI|Y;Y(1NG@72@Qi_H zz~-?83h(EB+d!<3cjBF#f}PI#ErPAv#W6c!0*vd=!{FodIs~J-MIZV|Sk4cMG{ZpH zU57r0c@$z>#q1P2Z{cs%Z!g$MG12BF;*a-nH5X_mz^0`me!C1A1`OXgFi1UxU9b-;&Ti5-wFp$gR(M^P$f znmNME!%u*EycZl5K0$M-3CO+;1)u+uP+fy4m0D^n;&D_Xx9na(sWfQFH}5DY%s4IR$m0!58&4}6*Wlg%s(?fk>S@9?tH`lChJbgb!uT{c1wP9VT0YaI*t?O zdShEm^V!j^?G?>gEuk@5??hK%$H^$YgjaNc&>NNlqlj|WNFiu}1A z(u1;tN;?N0YS047ZYrWt1c5!7CuGD`-}CNW;9WD%)_SB@`>vWTS;vvoHq+lP%~#;- zCztxu$FX!v;x2zLcRMJY%yW13-4?OY4ha`?l7eMH0sH_>kPgBR;};t@)^BSEvs+lG z$^B%&#YDS3IGK}NQ=qfao)%x_&VHydK4t|i1Ui}wma{~xq`Tj zP}qczaxk+PNLX-JQ9`M#K}$IB+LUc6Et1%|wrG41vRVFmSJ8+J(T>u>nAok+TP+MGd1=(Ig zHz3-si}@Z!lV>((9@xJl$$5LXw4JhEakMMz4DAi$vSZ;#c5RFDba5#@d=~J{Ger$2 zXtd}a87GTPiAy0eV$-2VGJ3<3aZ!}NEJoYAHlM zE0o}h`^lwayh3Gx9K>oPxia5WUtj)T_!DkSY-)_a1?(KXwEH?rOEYtdDEt`@yuNpm zZr<*D_IMpRcD%8@tE*xEUZA=h2SBd}3M@N-Snx6O0%p`fAo%TrOQ9byS^})om{-N* zoTV|V){Zs&_~Kda?Veu}X9J5egv+RfvyWBPYrI)&#FF50+@zr(n{Gl&Ae^N+k&t;K`+_#krG2I#W~ntl(( zKtB&Pm?w@w)A(nqj3)peTvYQ8k!UHuD!EczK~-dwXO`;-bh~dcnbM&;+QDZ`aCjZj zv{5*et2v-_cjue`=IvAMPuWMIP%iT;+)E=4dx|^+D--Tqq^(CXA3fn!R;5%40Eb-8 zvzDJ#oYg|iNEG+~ly>cLQB`Z3f!TY8{mu-X4<>bny+@Ch9zCXrN(EjLJz8j)qN$}; zrXVT;qQ)Q>xhT#Iw_&)0$W2kXcpZp6WzqdAew7owTy7*1#U@JR7X@b$-9^ z`{VoEGM9auHEY&-*IMuUKF?DS>sYtW}h^=9LR`R|8?J~?CZlsEtKvE=W4@yvS{6z;|D^X{WY&n(&G zo~e;PbYJG!Nd7Z91R&uHa+o%c?_!g_Vj&3nYjMD>-hxc~w9~g2-R==@`#tr013|Td-^vMu`aY-oGC5Jf@AytJ~a|>i~9gPu{ME9Zl=l zNAI$ikTSF5(3!)>+YV_r?mw8?4t(fOSFZeIasMIR$Sy$av|PR*e;`-Lcpg(b=B00%*z7i&WZSmgw#^*1Bhk7xTl+#(TJn4hjO^8IA6{mE zs{Y$qsWGQ5zdlR5nrXgW1D%X7+31qXr!7S#pxh|2em!Q}yf@Ch-TKk~PYdz~0y=DS zSmMi%{P~di)J|q-W#c9>X#(>HH&=F1ZhiIc-KLZAug4yRXwPDnwvf%1)4m(`AOe0g|Ii}6l^o4--TQ?;wTW?BoSe;g6Ss*tU6Ds$`p0|t(qM{-f zM7EuL;|=_^MMX;8$)0@UD(sDlYCF(|f7lx($?5KV<=h343vk}aHvCu4^;6DW6@>%+ z=bk*EoVyc(1nlKG@_DvS#+W@+{#dS;zmcD2D!_dnmHjQ@ax5!k{#?@a_mi%h>uMdj zb(Ss1GG7BH*}Ir<7PFi6bv76o_f^jsBRe7=LBR84+@T&B>dHGjvv1z-+F9Ktv8QEK zBzu&@6k7!wa32f7gmHxE+zP2H z_s2U&Ir)1{T-An#3@OK1WOJH#Ru@&2TG%#r%)MBi^D0CmM~Iut;~bk(`))LFQ(9_b z?uNTJS}QLY=vkzRuiVf83UFLs)CWk!^{=r@?s|F6tD^jMAn4>g=>RSzbRX=!jclo_ z3^DkMZr}%$1r!#bt^sVI=nj>?Vc&Q{5vrXZhS)Lab?;@yb%GWNbRv_u)!?IK?Tv!6 zG{QU!QCk;S*pavmM*rV)`dhjYsy7Z7ltBlsDEX!*%o7Gj)J=ETyFkguNBPPtmR~XLO*tGTO_-21Avo9> zY>rA&R$?zMG1*ub*Jl5r zb(eH}-?z_Q?!Q^c#H^4p4>-q|qf?)c4f^@miM5uOp_&zA!790JY4DQN)T= zfIER)xUnT}_iD+rfkz7FxU__+me9Bv3+IMupA1_!3Q&qa*N%T_vUD`G^NG)1IJfrG z%{Mc(o-AeoZto2?G*F(5m+(65+EHJyQ_8O_Evd}c=2z9^RGC|Lzlp5!(?Y=K04c!z z;p!c~NAWm}$4eP6m3CnC|NN2|0a1F7%iVPxQJA&BVuO{v$E7TPU3bPSl*thB8yqfp zpzPAIR=lGe?hbJOcpU5OwE#NdZ6Asd(l5r4$uSG(S!55J#W_kGrFO{n*h))u_c3M9 z!qXRHJ~J~t@>0cgN17uYCt}^#F>i8nh#C8k=sOho05d+j*xYg2!jxmSbX!`w>DL3e zRO%Nj_@`0!9W@F$2EwQt%(n}fmrTJ0$WmvK6zw_M#d zm<`z3LxJjJY)nCC0aRxK*my{bzSA?)SRRwSDkeo%FAEA<2J~b?dg7LNb7p34UZy3c zY|DYN<}(l$(qbESM(aPfx$MA}vKVd8H0Cy9U70iUorH$x?Fk8kA|kj`_shP`Nbm80 z?EpB-LBDbufPDh&2YCEL4D+3T%hK7|&|q&*Xc*)_V&*%5EpF#les3%*weLzT4Z1$C zUmh(j)xNnj^?DGvpcGN~qN0N$v&vRuzlPe_YXj5DHrdlsQ`795%F?cl9q4~gomfl?;Kf7J+YFO}CquV=hsypp}Pc@=tGg}sq8h4tKs8P^Q5B||s+z8vrJAjpt6HX7qsmnks)|(yRPCx`s@GNLRqv@jQhlPj zqPnH}QPr)wR@$R!&98^$f*mU1y%ESJb- zaxSittLOG`&D;sDo9pF$cnyCKZ{#227xPQ`=lK*qlh5Jv`C`6_e}zBEpW{E^KjJ^( zFY~wfpEUPq0yU#G<1~+H=4%#dp3!7zax@N2f#zk++nP@`S2Q;?-$Sg?qg81+EosMS z$7>(a&eT4xou^%>U8;RSyIPy1tXNt-!BC(!B_AT?iB)sQ9_U~S%?r83(JKUAg`4uBndl&UBXL3r*K*L zUbsUX>Byfh>2HjxYeY(-Qak_BbY~4~_ ztZsuYS(l}A>PmFGbh~w}x+A({x;J!h>)z3I=|0kZ1>z00-d{gJZ_y9a|4ARBpP`?x ze@-8xkJBgVQ}x^Rx%xtViN0QcKz~Btp?^!?seey@L4QeqP5-^V*WhdLGYl|T3_}eg z3=bHB4G$W^4UZe<8I~AU8sZGehHVC$A>RPwc03R)C;uX)(CjStuxEwhd7~W3fM8w5ciV<@m*h64H01q}n7IXUPd4GqND#$v2d(FloC+~mM z;`v`Q-X@DQLzD=Du3ht?&OKhB>4Js$heT40fkc`~0zrJ9_$dNCCA1PhT%@~_*qQkX zW|^SP-km;D2;|d8qI^I!Tcrr7Eo`wO1uyNJ!+PU*%&J^;n6X#2%0_DEWUBjDoc`HMP>+ zdt_#S)(#hp5UDk`DVm0YGJ^QWel1pD_tgG^$&vjf0~K9=+1y>~zJlNB`uzz#UFG<8 zQ~A-5cJAO&4+dknNNfnxqzIH$o>EPnCc7N7bQu#Rd-T=Cb&`F?u?q-wewh8zG$auS zOx<#{#(AQqxwb|tPuVHYJkMS=eYW@A;~i3FP5ejgKWWd^u;C-@rmC~&UFXc_&SzN9 zTI_82x1F1%mxjbo%G|6yzHskfLQL{8c^2G`FG_nh)GrMmW+!mxEj}?rP_#`$1f}Ap zLE7|rfvU?>k|-%vq_x7!BK7HN8*2smvV577D}vxi2b+2Y&+y(ZP#~E8#t1`%p2+*~ zr@0?B^)On17!fK^bW<#9QXoE9AcqLKGYz|E_;vSAcFzemzzlpiMp_veYj{+igXQ7xGygwX zwmHGF=+I_lCS#U}d^z-ox0H@;L%RW)i-pb!y$|mJflHIv z`*6NU`yr4GSt!{Lm_0v;%afvkoYL2oVA+O{dLmvUpxtjxu*)?HT~4tm((F~TXj}QX zHbVo?j>8IN#_C&UOtA-V%x8bJv^Y3uzZAPnh^hcDIjJ?Ma0(XwAC&&%qKWu;k*0{9 z#Y#g@CwR$vTyQ88dl0|w>Iav<@WS$ROpQR`c0;h{m8Vr;vFtGK&2K8$?Y!0a&m*6{ zsoi?&&Fr)0zL^MPRtNH(UueCK?|VN$!wy2#io5LP_KgD@rMenn%zL zCMaTvi99V6VCP1QJzdLCCJ4N-sRslW4cO#VIhyzu7*!Yy7t&`T{gP}o?L)G`Siz1p z1*$Nyrwe(`n6_zJ(PPr-cT+^=qJjNWQthwvOvqAjF!~T@AbzhvDdKeMF)}zvRXkFe zkAt=x**1S*VRA8#;LD2%&mq?)Ye&|g9G*TDkhrYqy(r8G5Gw1NEGiSm|Gp7_ssCH_ zmEMJX6$@FN$)#xoS~m&9Lu@ob_l{{frr4M!7F7)#%Z7&32r}?hlQ+Kx(O>|(Lb8b?K5>7cwCBqn4#vK^UH=`-)kv0Sf?y06OPjf!yj$gq!Z zDz)fxW@Ut(d+cN7S)zQ@#MB?)y$FD-StX)8;4}fXiP8%^nTRmm=g!22Sd%-GW(&#) ziIf%eW>0`3c}VhM%ug^wp)8E29h*Qg+regwbWylImb#a8FM$m%%ySN+V9*`}htpt1 z%wzyN48N=n^egT1DyH{u3RLeQbcdeEv1s}$1%J0()c)&BK54; zvE8MOHPzBp4Y6Uu3bbAyNo{GtyD2|rIk*+$x`0Dn6zzuapIs2#dXPzW||~?g3jWG94m2kb>+=1CSra7JNu!! z%hBNPE~wCn`&LwhIEE28@F#az4xQN5*=Z`bRqQC2$Z0RMIe(MgE-Ndmsx}$ARx%0- z0-j~sw1YBTyLOOKmqhU!H^#?rtZf8TP{bD6Si3PEp98vH{32a2O^TK&A;&2jN$1Ih zrD+x_gPeA185KJ7i(DpZNoh{3m#CrPu)QIHfa;ZqORC;l-)L$muCJ~}Iw~m4tEx#h zJ2zJ>uUuZYqAst_SzDwnY_(BkWg&*@ZXfqe&gDY5Z7HOa2@yVT6WnoQK?(g-U2Se| z9eVoT&OR{{39}9(0D@SL%aTN(P04_!-(CgZp)@Mev~G4KBuKP@!T8@D8lXPNVD#~0 z{KtPXA}7Y$J(7bA8HgZ8ax;by93Lyuw1zZ`7&tt?T^t?r>WzY>z#jlKS*g$d{ zOOj!_u_a+^lgI=HE2iE7_(aB5M~3-18_WuRMYN%Im{DSLg(m7-nkUI^^2FOfR7@O= zQ3S$*;C>-Af29lj_D6qL=mRdWVP@ucX~m*j7Lqj>&o;v)*Wto+3iz1rj&y;n$Wws8 z6`Why**RoNhEcDoz#JKEgSKPxJe81i-zB%Bwf3n7!g#O;L@W`&sD6_GSHZaiX+}Ai z*b`{PNV~IBQDKSGQc~rrF0ImDI7zz{yVwa$RCKO@0W?@~>nR)f{|GpKHXf8|;NC<2 zG8r$4gp3M?j0y@wN*%%Pfnn@n8|U%Ylvx{+tJ5i4o1LAxJp+U>yfuZ&tAHBOxblm@ zG+jvYvv=ljkfvqN_RN9BoQmNC0|<2|V$niI*$+ToKb}}5dIrz@Zrx&j7E&O~Xq%!D zm56zHf7z7BfX^vDF*$LoW$T85^*fU&ICy)od6OKWXcm%7G|1DT*fxwzs1H=VpGa72 zX$WA@VI<{2+Azb6Mak>cZ=D5v;O(PqAPS%d4$Q%)W!MQ+DL}icj~%(BtQW8tA#Oum zb4rU@DS%6{3gd(uXmBQ!TNzwQ>cwc$Vs2?BL}!0Ulp9T{xg^h;2%PiCQzdT_|D{OJ zF2v$S8JJs}U@2pr(H2sJH%cx7{S!}S`IUw8cStaqc=t;}@AO_`qXh%pZPnPvx6;1= DgjuED diff --git a/app/assets/fonts/FontAwesome/fontawesome-webfont.eot b/app/assets/fonts/FontAwesome/fontawesome-webfont.eot deleted file mode 100755 index 6cfd56609567bc9db55186415c694d1d32808fc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 72449 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
U~M!&>g0b zP>w^~Z+#M>N%zq`RR3r6Iv_h2r+{E1$_|AX$BAqu#`-&YpsT8ToFEi#V3WRo?=Iq0 z;zSKrc0Mr|!-U7{q!e`alPUc;ZBIz>eNdu5UVcipvm~Td!`BN12uv%2Y7p)*4jM^3 zlrM8uP;Ra<2RxP;hNh&gMtNL;lLqkQ} zRR~$x=MLTIN|2%rDk}tHjJ;7ZWI}a13JAx$*A$E9B&T<4B6%_tZ<>UoVev*xWVl88 z(3WD#{A5=lV<~~nL{F&*3y{RQ-K~~o0*Y5C5=??m4nwW{_!U=ei~IV=q@ox;?O;Eu z!HbAZ!j5E>EUhHeLJnH~>&VE!*Nb|{Gc{b!iE|A$JR1Y3{}5& zVmV0E@Dl0BS#0(>H8Vrp4#H=gIW)$GEtn{i@(AIekOdlhy5+QcZ=mzSL}*tsM*9a?@Q^l<6kDFh(XPMB30p~vDD$zx6^`y@td{B@ok@l; z!N(U!wtN@$BM-IZCg8_M^|M*q&s2fV!0`HF z=+n?79pUpPL#Yv~slXpnSb&9!+(ZIeTsla}@fa+RJ(R9#@JemkJWpC?uK2Ts0q&u9 z;oV)Z<4W2Sir%sN8yoB?5r_~UYsc#a1fXdUo1xi+rYP6-U%MLXXl)SSdau8A_r!iA zC}Fz^k1gi+L~bun+~!XG&Nbm3W{D)jq zuby5|i`M*}|CWFf+$ea8wOB!*DAJhgK$0Fv(i}u8J0sWb@FwK!#$PNIm z_ZX1}{Tav6jRJ1jICmcClETGh#v|kwTil)yERQxf@dnEI3gkR{N3iJ-)Zy7r5R;i> z%(xMzlh(vYF{9Js<`keoz*#xx-iEQ1SfxU-CY*WG=*pkS4WJ6en9*}HJvc@0G^}%0 zE#!n`oCl}*v(;P=1J96tHB!`1r>Y=PSX}yXYhUg;lXDBSWL79>lZWg5qz^p&n zkJX;w_=tN`$D$E#$`$PD>l7x}ABH`-8$wkY>X*jj3|qf_^5}L%bTAYw0wY1LF6`L9 z!Vv+%9N^77O<;QVzF7IFYI9ku$EygDeA$(Ik%NLIu}+9t@TP|g$ngnX11~&%F!z2n z(8sz%)@751T~33TK!Ht|X=I?~6dm%BTrm%7pFS4Jb48mT^zO=} z5bMV8u30LL5`*vajWZi28`^a&P!Ip@!nl42or&p=Jsh(* z1kW3lXMt7Pe!R_&!ZBXD)al@R!Bk^9BLqj#kXsWh)X8T5qL6EbE_5HIDo0w(z>%n2 z30(MtHN5b=XUR@vfiyr^3`HKlQGM-)v)hSxk&#q83;NttQ`)Gw#EhCZ+}$074Ez&; zU=+*yQyuXnvBgY4rP|3!y^H7+DK(z{_e9+jFPgdQU-^aeYtio$G?@c^gw;iV!HG&T z;l{(&+IK$o-X4V~q;!syDW0-|ZyG11*>61)c=U_B4-$5AQr@3$X%R;)^c==IOW-C&@X` z8~T=1pnh?2UV22f`Lwg@$v9Y4fJG5DfM(pWE%ScY*iR_;%An`Mk8Fz+xdj2bOG%iN z82lht%#<|Y;uT+E`HL}XYM3W%=A%Vni`gd6U3CSughYKx zg?qfU-UZ~a*nosPC8+KXTyCv3wq}pjNp!sh@$bumNM_K(5QBEf>cHCHrsxZ_B;UV^ z{^qt|1FzSMjAzFz}11}UKx^1HP%)_zQo;i&L9`d=_HDl zv2?mED@^#)bJ?E``auXTjfa!MxbsN{tGb29bz!Wc8M7{9lw1!sSpt(Qh5!XeOT}*$ z&?oi-t*t)A)&@;H2TZj;F4TGW$-Tlk(?L#PD{cgtfRPr9lGu49gx}5JH7t#TQ-n1wq6s1X z)f-bDwQSlHj2L{6(*t4}baX15_|j)OdLO`+AY0;iYvLiSU4GKkk0GO6DjxQ+ZL$^v zQH{nJ%euuu;#_S!sdCZHseil*eG*b3t^fQpi2-IH$p2iq6Wwq+hJB0m_;FkAaWDH* zu*)U!a^ay|iT&?MseilDIEK!}!gm%(LDiFd!QSpHV&8oi`P^_NYud=ESwK(F0j=Ch zfHm$6wY{jtM@(k}-)qeX+JtvA@aS@fEIBP$K^yrp#U@um1XblF|Y?d;wbpNxi89zlH}`;Ahy{_NB)3UggiWDpLlepwvJmAZ_GX(=OJjU>@M zUgyws_&G63;t);Fk_4eo zSu0Y420r3sr@2tfqj0bC8O>AGWXv+?d-T|}^xe@IW_dI^EgBzUbAC$;-lX{_+(U5> z4OfD9J$I$sLBe{tdbsoAU7H6fZ}8ec3rW;FZ_vGkLsVQ`ESKVqh7_xX9KJw@-m3O2 zLszjiH*DxJAeIPTWg%5`(p8S#9_AR2QWs;y5QTfIf7*mzi!}kUD+;9UJn;eu6#t_S!rV3Nl*jejz@;ALfpkm#gWOZ%iG zbE?;1{~A$vUR5T5)FS0REq)N`QH56e%rNMC=7Y458KtkI?USd^p@j-wR@!gbzx<9nd*0}xU8AuK)0*4^0yq7Kbj*smwZ zEgQ9K`n+48tGHthmL%P_QM1P!1Xw}M$B)dx=B8UYbo#95Ba8kC`m8Q?s`I}T3z1TS zw3-xg4f9p&G$yb12DmmC;SAequx5nWvDQ^%9$Iim4`D_Bo7MzlI7f8Q} z7#mLR*-V^ghp<0RSI`aa3+LfIG2J-GV6MFdA=u6>P{CWJZ`BoTX$Jk-!`F-N=ITO; z*Kh5M_IN(B=j3KO)^rs!>9Woa(#5dv$BpZ_ET2{NF)O?qEzRTcJw-}ED8CD}+^}Or z*Z3u@EE9=|1OfZU@vm`?IIDMyVvZ~;qP0v@w}|i`J)MwDA-{WYvyd2SG$Up@eDP6q z3m*$yr0g0nF8L9`+2Tq=vSgiz9})k?YZ!AU5DN@B2P(9*<556wZ@b#QMZL!sdor<` zjYob|Q5yH%ClsKkzr~*)%zdn0pZZ zkK7Ray}9`)hx2gJ*$oJR;2trmaAK|qsM5!cTWe`Lx$9f?FI$Cnq8xn{lrnz%joQy|oV>F&4BqXn7ywxi6{a;B1mzDl!TRmo`says!4D0yE zgJCIA75dQ9Mb^*NT_8acrGQ(5l^WxgR$)mu`}S!J8v}$D1gb}IA7Dn?(G$%z>r`c=edOKKfB!A?rFgFYI)b-36fF zYJv20$Ni`mx!woNG(!`F)>=#D(Co|-DQcmqjnZxwOq!e8KspChU>@ireQ2nYKB^3@ zLO5o$)5!^im0H0t+2un>l_f-p6+LCw^Z`9HZbE_( zJWA~Ae>PuOCi$!}Uw#OS+eZ*XGK3v}&9OnXnMft=f%8q__{^a8(9)8Rx@JE@yY#2* zGw36Y36OR8AL-ApwDIKJTDHMnE-Ob@iiDq6$B3XAHT6@Gl~uQC$HAiuOVBIjzQ=kZf!O>&7QvoraT+c z4hC1w#zT&R;km#z`5M?Ve9u@REm~Pq;eglc;3zs+iKxyqcFGi0q`a-Gik1h37p%!j z`Z3HBLChRizH>S>2VScPRz(EC{U#)uYw-SV#%&)oI2XYMBE|EwyhTe9tsn`r112LB zX;JKmu<+!fGRwxcgb`H;(G*ulx}AM8Y|$EvFow5wCTfn;BVX>U-6?4P7|>7b6F|FJ z-Z%F-x!qTf0Ij%TTfXgAZxK$Na^U%WfduyF1@JkAZz83q?3Vv`Q`!I~u#Le!Bs~ zW7fggslMo`Oxr)c{XG%nP5P^jZcs9@uLN^DUW_qpnGw&MFtN<_f>7FbYca!~^Cqpe zQ#M01mp&Zc0CrV_Qt4B7FIn)pz2s?J{F*!M6T`;BultJ~h;4GnbNmP4eCn9N3ZE`U zzGH%0&?8cx8C46i$T->!hz(Zn2GHWd1&eV_(Kz~T*wYbU`&7SMmYXC;rxSDgD84pi z#VnzFoY<`@q)9J-l_$6|+l?XvzkuhXbhNaiTv5 z#yR%dEwzLJ9|*D{Kva%+R!{mJmhf`T9$>i0`Bn+v$9eSp7ilgAdcDOVv|Fk(pY*d* z-RaFL)aZq@D~U252V8M`8DY~YWxyl0Bs;WtJqP@0pmV0^Kz}O)l=jD;z+5d9 zYR-?hfBQPgU!oLB^G{!Um{LS_9KD_BsWogR+VJlnLs!Dz3J9%q)ExNyZat_$GHY+b z`M|+1avEKkKLOiVhQ=8ugxJTPLL5JqJQs=SwgzR^uHUrL@R}87MGEp)yV^!w;1J13 z{kl9&>{SJhT_|5-A|rfd#JxU+N)5txz-jg8XcdEbHWH!VI$7FI9pCKEB_rX9CGPxs zJ6sB*3p-qj`nH8Q;iKid@6LBSCQ^$CR}@oAN<}U(hu1|htWMd!LQ$JCzRyHdzy^gi z;zC2;(oQ}*czLLzx_ihFk-7}zXdnupwJTf?ChN#G$Vn@TH({71S|FBRDin65 zohg&uhaU#2&)cWBXh*6=+S*}fiU@hZEvMRKXx%OdZ4NDW3t8WZrC8Tz@jTipej!JO@~SZ~17#kfSvwO`QVU>qc~&MR ziht;9h(Ri^_#>pNC%KYqtI)(UoX=8O29owdbva^WV%=6`t;K<)j?htxff2kOB%sb9 zhZ)T`NB=l@Dl(K|r_o^CUj%oeQ{Fdk1T{5-gWOqdSa`O)^bY7yTc)#gWN(|D4_ zs2f5RQ$2g{x_PR?FvT)qP0jl88&B`5I`EL?9Q-q4yDFS!Y*N~4;1{WKJYfnnc%Gqd z;?0vU82Uv#m~lVC6w_0ENeTNqPFXv*uk$3MT>6GdOd=L;2K=hLUNVA*(=U8?;{kWa zd7u#o5Ij4QR@^`Gq*V#ElxvsX&{WSmmp^mq>UsObckd5gD=dkDg+GV%Ao@vb0=I<8Bs{TYan*n zMFo}zW>m#Rb6fhTX~h@U4f0ZA>ZPAq@~Ids_RfXr{lqS&U>^hGzXk(FC&Gq+>D{mU z?tKNLbgI~FwMTK5yCre4m-a<~Nhx-Q^KFd@C@#8)-SL7K9bVoY4|(+uE=r0Xei-Ko zq=^&uNZVMz;tb)UsAYx`I8;`sozTQg#}7~EsZVlyK?07QeeX}162oIT%~fOlEpG>N zMRPljQSB@|!qLAn^ZvOD)DZCJ$mh)e)N?ay8u30My_MS+zsoBEOq5)4g)Xi%~Vbh`D0xgkXp&ubVev{so8xFgt z?T!hzWm4kbN#LLs>CKdhaDtOvJiBYVza&{>Qk45{1z_c7MCadi=wHNkEC^Qdrzr{$ zvML=bGRUp1>!xTJ51Jk`;xIr9e?s1Rbc^#b?xLjiVCz`<$00-Y62*wn>KT zRAId;{M2!3e|}`3`K{-UX||VRsezlned3iP%{NEUDy1uQXThzIr2^WPgZgpW3#gTE zQFUDe+|(PPEo(J4ddaq~q$rkCO^R#Zc1=pjns(SU(BMBRjHs~uQHdT3TjhNepyMn$&oZiyNQ#TvZDHDD%Ml{v+5oEqA z9wF=eje)UMKgGicCa}Pb5=8WXqMAd+?3aDgr^+d1=c!|kS!k-D2oD5rbPO``sc~Sd zSnp?U;wgg!1*zkv>$&^QJP0GQn9XW2vWsLO^Lvo9yz8PZZY9+{Mc`6{G`Y!c2J)O+ zewh3U-?38QqVdD41G+}^hkjP~$ssQ9wNlJVL89Q!oUn#q0I)6KWZA^rgzWs;>Gq>v zwkw}^#ib8{0NAgQ+N|x%#ZL@rmisfs8@-o$*<8_d37I3`sYBY4(ZARKK6{a-+-zBq zG{T!4{T?u;#KxOH3d2jBp}#krX$U#W4y4dE%v>XPFw@!Y3?s28*r{fIaE_!<3`N&g$vOMt%`9k=+_l4DD?|9qSA6kc>MC5%P(Tb=P#pE0+|BL5_;*-)Mx)tl@kSc#$J?i!PwTSyVK%V_BIO2jnn-(?b%D zXjZ8;%p+#|`qU$3iznWYe7m$#YBjMHJ zf6YmRHNn5Ay;JidLPJX#sICe6a*S@k#r@#^9OdY#s7j?_F7$PpwRoHs7fgdpsyaw3 zjOZJ&EUUDjnw;*;U5uz%3d+#4%ghFA=_fqRhAH^_g~#q=FR3?Y;mOAo8&+nSQO)qb zT8vi~zXt-H66pI*JnirE+(S|Ady;FKlo7Q9`J<-{#JpF2cdqEIPFR4&ghJxh%Mxu1o(Uelk2x%6E!{LYyoVZZpGQ0=jHupM=>)=PWOkfLQvl%VUWRGAA|$0F1&vwasg- z@VcNq(D*Q}eyGOHLMCTMOViB(UIg{+72to*en28k zj0oC2e~`&a;5BWk=h5j;fHRWSgl#`s`07#}kS<$Rh!Pqlg^5OYTlaXRi?~})!tWD# z@v%=8P-#ZOUT<Epah&sW^m}#g0SdzYY#&Od^KblG+DZ!UNR}>a7#*OAT2&tFzUr zj-4(VPC{$Vwi;7Jm~{rD$Rp7D*S?upf3~n;7Rlu17;)f~_YTNr3eSxHN zo~H}C$>dKg6r%lN3cTfV83{?C<_q9Cgny$#ul(9!*fhn5f4FLIizxnJzXmr9&_kv# zf2H-J@t2G9X>a%9VCC_%BD>NQ#EAapu35#9L$2&`GOc!<#-20fKYY*sHC*pVGkptX zb@#(3z2gCt$kbkcJ%&k;M4vC%=RR>zD-+U;UjxFx$B;Z>p79{G{&JG1q|^@QZ|)%> zHb|g2Y&O6FR!O_}uxV#6>rfyseLE(zj~jjTbVQVN6JVc%CDYV=C_t;uXu}pshjfDA z&<+bsG82R1O04`cCxQG~u@w}vVT+9tJtxM$>N`Tk@!4r>={zla##3rC15X(<=<;v$ zzuW&~45fE1?|g0gSca_6Z<5RkFFBu6m4KF&>7J=kd974|_#(%g_eHZECAs98eLWFK zyYeSTL3eB~UCU5{N+;Cz^^$!$eAb_|avekPV$$-0)wMHU!}u|P9p=rWiNhBfEK~Ab zAjKpm5>F6%H69^{4?rCnKqtY&M2G!u(}DDYln}zt*?(XRjxzGi1GS-A+s^H6gDScy zERY<=pcs*b=Lef`CFf+p%_N1eY!;Bu(|vHG?F02-0Zwi}1o zns;&O?WG!5KWNT|mxX6gh5QY$qpQPnQ#zl2l)V34(xxX=&sD#t5o}n(>|b4zO6}!r zenh^;qzHYp^BQq=W(uy^T9X!p=1dXXg)gsOL&}+C2Q2& zb}7H5FxSv*e5bL3L3%tbyK<aYP$hd6kD z?||pdfGS3vHV~JaAHVnsL!!z8)!Og#48`*DN`;!yd;wJ!I!MqBFKY;OBzXsI*t4u*VEz;?KkE;aFxkGIdN4~%_Ge4insnE z`K(VWO7x;zGe_JVp$}|P;8hr_2IMHl+DL~#ls`cRh%%Ysx3(Dp0*FGJQ z&n}Q13Vzl;@^K?Ow(nE)N|W_;xIl;zxwKqA<%$d^=U(=`7&Pp1$*a?kA1y$SNoC1X zIUpmfs&G^wql9@&n9@FHSf}rr8J=^@uXcYy*Oni#K>;Uh1=wfMi9vOmDjaj zU0vF%zu09ehjOus8vQcnYF1XipVZJ4Dbi1kGnb4j`@rJzPwD2u2CcPbqbaX$FyTO$ zhF2i7C4W}-*!V-ZATAlu6k`|bJue0}m4>>0znpScDwDauxMcm4k_w9n48uGFA&zw4 zHwmq>=gC43e{nEwI{@{s;RJm_Bc(abg;7-{-HqACiaM6O?)jS!Cj2UUi*Smd{ygcl{TlgLQ6MRh#JBy_IjI z{?WC9{eWiO|C$x07q0Oxk_rG<(<^sAn2j-N4A{&fb_Rqtf}t9Wk-0SF>|dJ#=8!rQ zh1g-28{C^$D{5Q4;oTJkv&B;kta((PDg3reEzMTKq;gr^;hObo6jEyXTyGs`a{0K6 z2CHkA0@Kee0og(*ox;OQtta#lD4GA)P|e>zi1DZe#;f{T!tnTi0-F%2(dFJ$vmE80)f(Z~>{B z#BOt-8EPKjK*PXs7sa!L?^Qu?*t0${WQ~I2d=G1Y6@Z926Uo)4{>(Cx5f&uRFxu*( zn1sBHiis3on+-W6DzGzGQB?XO*F&~kJg)j94U?}|wqiy|)L4WB{H?8)pge)UzsMiN z#c(e089Yz%R(urwVwCJr4^j=`#wrdi)+WOY!M{Q=pl`$Q1lV5LMUur3p)SH3kjp`^ z7LbR@oMGYoCW6e2^z}`p3!ID^C>GsOvqQsnFXv1wNE3}uaPT@5ZlS^_k%MqyR5+x^ zJo;!S)mc5oR$a_u6heEa1z0-kx~?|ZScR=P!#Ute&+Qo@i9D-MtLFF$L@J5mse80o z`W#~mum6>UVq`hYi9OuWmR+}KY^k@#^{k?tKq8298qyWkirl(H;-_j2pru&}?5 z=-wt8S~C4|fg3Wz^9<)?i0syCv2x=ZEU;Sr99kMd)W1V7BfkZU3C}2(etb)2cxr^= zpwZj}s8ict^}GE5vE6@o8kM?ycAm%$aO{N7Q4(Vp+voosKaegf^jPKlreOu}Q+jKgZnJ zXh-^QU>z=#-p=?*=c?hheYA)B(cP>rGZsOgb)laul6y29Ryt`FQZI6TX%x=e)nVVD z<*;*8YwImd6U%pV{8aHN=E@rod!;K9RPo6+Y=++%6()K5y$$<=w&kn15BbwR9FT%; zXH1Gx@dAsXJt!dmLhy3Fa|&C14E>;cb;bxzA~zi=m50e`Q|-WI#odRlFBCpl|3u8M zP<s0r67)jLqqeW!pMX2r7_gXy8R?ZL~Y4n$A2f+KJn|#e22b4)mWn7$!1~IdiBNI=r zhX;2iLFfFD^OGDy4dmwV4Cp;v%<*2erLTU{qm0Z&wDKZ%l$+=6lL@z23U45Ct`(TNN5cMGxi>wh@H2e`0 zKCoS2DJ+BwVVjbJYPe;?*c{a{pE0AIu)-?Uk(viV~41~y$UhB>a$EZPf@=HxX+y_qr z$=rmlXh<$qn%;~U2WUxI{6WKRH1*~tewDo@E?imZgw{BR-<0=+u!l4M#d3qFi?D;a z**ZIWbLG6C5pe!XYP#k-s=tn6zvbU@mb-K#0jP3MyoD3}zgxogneGoQI*&nz842SP z{?8tTn4FUBp8 z91fEpf+A7x{}Ku12`?%FVyPdY%E2FXrKaw|TiEd~{Ut3sh_b|Hxm_GEcJG2Ln*cv+ zZ?fl1Pijig=|W;J4;Z643fiB6UZ2ior*0kL*wwPbYdt^68Rfnn^PVMtWaW!m3gE4% zn@3ovVk*J(Q6e@7Wb&g>nNV;UfmJrgT`!tzH**5XY$hSoEpuw^7TKnft z?M;@4XU#SZq>E)v3_sfEs4Ok1M3v~F@4>eGwYLE(%(I_JR#WiuY`iu63m3g;2Djvp zuJLKpDHG}JRbx_<93;Ob)LW~rH{Xp^Z9Q0ij0~;F++v!WqzDd%P`;yGtj%)D;+L_HK=Il(-YOAf~_COC~K4_w+n(v54UF5C*&7r2`=)NqMkc}n`Y>W8? z5x$pVo8&m{L|EtG5w|j|s6-sMM;ya_xxpP4A>yLkP)kK{w0#JZU2N^=LMZnbp`>}K z_?LpBU?-8mFVbu+Z3U+|E}kJSlrc>0F|@s^f3X5RRFb$wApO1%%C?R=ZpIAY{ll<4 zy}@BYbIT9*E69_IGUA@$J>$4?_XTZnj}Rf)qs`F{ zb51=?v^(cVvz77rC|uU^!(J7nEP!)YtT>)PJeE ze##uiE8pV|BnQV(dTYQdSduIis#THcwsz@;&Q&(wVRo;3I0YXzNVU)^Rfkkh7dQ;haaajU7y*jI23N;(PWPcFHq^L~ zcn`9%bn@PihbB-&XAQ~rDU!4Qj9I65r_mm(8s4_TOtKl$VFrBK@9MYi4ii(7!!hqd zT>a@@;ixoHZ)&?`X}ba!oF*R}Fy&#ZVv9EycCS6F4ih<9$&Q!hlLU{)F74}D$%Q2U znhE*TyNEJPAA$6N@opiJ1iX}+fuND{-m@DWL~CJR6&R+Y;l-TTYMC|O>gRhy%9w}o zfRuP12pqNEa$m0_?}kGj7I~+ZA6=uqF$<+@zV1d*&r9D8^VkaKSxMm_bH&XXlOU8C z{r6fT4TnHLf{%S~I|BASfWz+}WY;hx9zGvoGDnPR5v%p}7pKx`<+yfA7NyHUE&-^6 zzlzBsv!FQ$HX*Bo6prILqZ!^Qa6qWhR&!~ZV;F+k40dZs_} zor8&3k%fIPsdBH*lqxPqaP&6MA)@z=5gZMUT9~dg`IAhy31ya}`oOf3l*fSMWmu}p z=1kz#O|6rF=d+1lS=}rS(8^>>rx=MIHQRum1c^N&gd381wb={qED!xiK*U%U!!aPK zVfF2;)>0V*NhYfyB@;9Y^~v-$78N^#*+3}7pcsuLEGWVh#-lhs&`iHzSp*k_N|FTjAuSz-eO1|9M32FYCb=^TD&C zv2bDJ(8ZBJM-+J*`-8g((-2J3?}Sq};TIy!0v=FLx#8Idd}8Lz>l(2qA&A3ud91}! zR8N9iA|=1)iceso$a3|DQrhXGSk)Dc1OQ%?uyINvSyy7pL#CfXzCafDBo|eg=+hD&JJ@{^7x-206v;!du-$`bV`+(;nJAt^ z%{Chy{qyi<4kK-S;og8?RV#wCGaY zsjO7`bXf54d3*Ls4bg5gW(f?c8RMi;QuKme3n2g}JS(`Mni}$+eL%GM5D0n+@OZXD z0}V<9f653uG!z46#KvlmD4E$2@Y*%mtB0QeoD6rP-=K6r@2sUe5r~eyfP6ur9+Ukv z!CGs)#O*j@o)7^vv%)wDB3M81B7z`SaxMOsITsS)eBp_TDD5y3A;caS)eDl8z{7=w zB5&yV8*ikvJuWF~$N)3+3=8wK6dBbpB*fKmrf_#qkTDvzL(IgES*Wsq?n-;iPEI>>7J$;g;D%-mCXDd2QEUSr6nhX(AHS@Kc5?lzQ!~Gf7)56nej&$;o$B|K#-K=OsCt2{l&_U zw?~#6gBb;2qi5JDPfk-F0C?{$;-~5P{slW^vI;iIj2(z&sC}!5G>nKLZ)c@kkg;*_ za0m7{0&j%j_u^)CL^&uhf-uWhiMFqy$MPG7czvsnIgY4#8tDWzsCcuT&Y}3fLwDq=Cim+UB@O{SKEzlV!E&Pk0_}kYz|^v@3;v7= z#!O$^sAzL4h&h#H4f@@x7j<5q5xOC3XTYGYAIGxY@S-fC2qxc;ngDNXNet)vw-*+n zRr?=Q)KmhWGa10jcgZ6T~ z=6M7mSLYydM{u}FuFdGdLm`}-j+Y0w9Z2hLKYG`8 zMx~B`Wd#D?<25Lsg58(eIgtecyB!w_ACaWUZrd{c>IdHK8z z@OXk>jYweF{5ovV-whSU3o1bITG&&z)S6?F*u@;3u!NKpriS!!ESW8>Q&=9NZMw}a zM(!+-B+czAvPkTRXBgx`o^$cOG{6%=`)b9X$8^vJ-CzOGO#s1B#?vTK z;0Dw$LnO}lk^RCF21^f^B;Z=fr9~v-E_v<(&1C|~$pH|#kT-MOoP|VIBMgvIVIKC&eJ{IghYhp6s&L+4D9hx6g>ZfTl(cl^(LIfc#kxHSX#B zQSwK2coNSEt}VFfu{2^XS5i0zgIZ|OZccObT}?p ze43zDm|fO+BHZk?DU{C}DLgJSfS|OepoM|SC|=kF`VZ2VSMi+=anj~c<_#)ihK`r+ zwV5e_{9kvU#EfzvBG&(g+^ES?P6$Miv8+fPWbnzEKerwtE|S3?bjleP^syWe_N4q# zL++xX$^8aC6&h#Hi56+nJEzu%s~QU zvP_2L!F(c0C4&ec;JX(&jE!adJcXw6-Ps|ZO;kB;itmr7NH~qbz}l{k5(%y z!a)siHj6fuvc^v6j#ef@*bvRSSF#5vjbxcl)2zapokzmUko*W~NnopEKiI8${@^W1 z`Lld1+Un?8JX9odR1sK_5NGiKu>YwcT+svqDiCy$vV$uAhd~H7f~$fqfET`$fco}; z`4Vl{=f*KNz)*zwcA*I%_f440D~^q<3safo3g__q=~~o*4$essgd{G`$n#3}!{LM* z*t@feXAGK#2OHs*lYZ*>GL@)PuCZFF`7?Ynk~;wo$WgKxYy%O)8Y7hp|X zq@*{GpX7ujr1k5eb)1`g+rNamEp8N>gNNSYfvD?8nh+Jiu@ZL=R3mz4qM-KB=)bzV@3K<=`dYuvv@kSXyQp7RA=OJ{JBL2N^$sAnRfim_N!rn;wB% zkEH*L{?~kIBg~o1;a3XW)xv=2fjYoL;<{%9Kg-7rOt>0)5#>%dW7e0MrI!#JTlkmy z!X)k{<^-Wn8FwI)flOXZ`lm#Xr1{qk9ikXw%j9;UN9W|6*{a2;Q^SjE_>i&jp9>N$)NrWuDpq;5`+qa>oNKEWmi8& zAWV6=$Y$(LDAcj|6)R(oC9t%4OmNm!rvf$ zXFx%K>}W>KoWr}fBB-VzJj&#l>|BB-V&OKSHdzP}2B2a}BLW?P6}StgBJ;AirXW9< zO1xz;Oh>JDkU;Q1)5fCn_%t{lzmOvpoJm56?D6RZm=MuQeHNXaVVYnpDQ4x=SLFi9 zBDmF)aU@83P!b_>pOrBMPkmsS7%XgEVvcGYF;&b1T7DLWMqANlJ382@fWF^fu&8?Y zEt6T?j8^!*L>-$|MmqPARTmM-XJ3F^s%GOTu|zC#}NXtC;gQ zJa57>2q((pWE5#IPylbmj38}6d@yZz--Jyd**?HNU@qZlmvq9HNOM7x&yF#uC8ctJ z!)d>>E%CmjG7rwQVOEyG0m7d>9Z{wX zj8}l51oxuS8N^oLX_5+4)MuhFXjFk{_0hcR0JGtsQG-cKBptAisM!CCA-!RHBgvr> z2uWI+GHeOJf9W*Cgud2qEo-3hLG)&LnkZFtN=K*R-xl#wFwkEcvz&)?%HWe z>LH>|&&M6RVe}4w;Pwtq1`8FJlp9;@gJeIUjJ++p94q7J4#t>_jijPK4?!EUJnw09 zMFjA#BiJH*a;Q^%p{szGE@u&ID&@65qJ%CguE%`1-A~nj zh<}Y;^MugOmm;)9|GuX^r!BmYmkh|vEv7c5{`Fj};Qr}gKx{;P$;X#4$3>DOK#NfeA4ekZM zt3Yt5*LS06ztZYY#GxB#Y#ZK zl6XW27{5U3X<;z5R8T+HR4*lh$Z_vP?DqM zs|IGxs){0X$d>(4$a`N38cd)NnUo5gj0xmUE5v|fG-h!Iw1N_og|I56O9ITC1?YGw z$`zyNg$W>JFJUBD1OQtD7kj(PH^t*xZRdcJwR{rpb#5T4A-nNsa3`BC?m$7`7Sq>7 zu@{BLE*NFCz&22SC(9M2c=p)iU}+;ZZ@CaeX2RXo8lfzgHpGS?xnGk&VkAx%j0KDn zLoyPs2sPh^$9_^#_auvZd|#oF*>o-;Lje=Z-7BSq7!)L0Be)*%_k5sg*o#EZ=sYrI zGBW6wEhm-v%Z1w_h=0Ns3lHFla}olscZb71BHAFy<3D7Yh7>u4pBF$ZFG2MQ?L(o_ zY9@+la)>i%O+0{dAdRuLJ*8`dqE1d5gt(=LVl%;5j2rm0KA5j84N#~;nv&r36Hs|+ zQN)q@953i~g(up3YGwdIKv0IuBhoYq1(h@}65ik(0DSgGuKPJ2n~Bh%_8vsg;!mXZ zYcvLu8Ez^^B{4dQD0@^%If*jiTnXn?_#E2)m-nv}_^ zZLLSZQamJ7y3_-Ww-=!b_`)-WZqwa`1Op)TuH26>a3JPEw0=?n9iUGN6vI9j`2>j{ z_+cP6UnQCmLe}cWek_LIC9)u7Wa_s3* zG4TXtGe{6Wy@-2Qbw-**`*fi$O;H!{aY&qoLs*`d;!U@4N7*x(KQ6F{>G19(XCfi|4PmjSYh z9_nCn5Cla&5>D&^6Jd7?fM`OqljZmg2uV5k*GvQzk{KH!I)a&AQ~1EFZGzVY_lp+B zj-@M>9s&q%8;Aph*UG{FFQtRR!ls>X*zt@Do(8R`{IMZ~)eKngll1F7RLH0mN-l*e zk~&rc%S?=22_=l2GDTh=Yz|Kd(|*O|wc(k+5rHK{1(^jalaOd(K=M0xwWKC)`U}#T z3Wr_O`;}D)qI!WvR3o(%d6CTv=+#ZlCK%4?DlT3ACMc0-4y5==37^o8u@Nz&$&a!^ z`ve?_Iuf65Lz#=hBK9Gk(GU9jXg1nvH1uT^6NfdCVPL7F9>o?%MzlPsg>ke@0Wwc- z0xTRbQQ;Msp}Ikt;c&4XCk^CoVwnnsEgAtsNS2uZf|k|&?CCEbYyoz+OyCTT>_JM@`D~kUvr6g`=Hz66YIi&mt-Kp+cq^w z%jpKy=oQK+Ol-NqqEsfu2W6aoHM~7E4*Lh+0^$^EJk3I48AR$aQVO)3HIVKvB)mKk zN9$56$;fnWa)`81mjt6iUIJRIc`XQE%j1AUOJSfFdl8ct({CVQ1T-HV$_If#Oui303_GNK(iHhq`N4$LFYOo}cFoXpV z;YicWQ`h6Q0fp@T?Pjv=ebr$I^QQ@h~PPQ)Y*eT(NR8}Hg=epa=~ivm*QKkrMiXJhc+`> zo#X~k|IMjbDP$~TuzeF^^}^ug4WS`Lc57Bh!BDXv-K-W$P)ChfB!{Vhbl}K_V-uFn zU1L*ZB}zmdLJ&Ng4je@WLlmek0Tk9H01zxDCk8)`z$PnJBDozUfKI(^1drX|^xm<(uY3T*G!A%YTdQZ&il z2hR|R5qWk-J7UgpGF8xk(DyG6_#8Emhymkmr=#(;cz#y`OvDohHGn*o*i8mh3jWPB z3Z$i&eBg){qeQew(M`w+H{4d8pGBI2@|4*m#2N+q$y$X{YwZZ1<1vr42&4B~K6WRV zA9DpGmz|Z7MiwWKET-tGsXrLK?1IZ74AHm%ZYDLbKoCQs0vRPS5FnMI;>6$*0Mkm4 zRLed1+a;w4(sf>hKmZJEer$q|`i(nQj)~7E=taLwO-3Fvh|J?mt>GmU`OSho1{zKI z8(F#ptn1q{ZcY#J!FW_$Y69n5@=9kcpc^JWP}0yecpQz`u^al$<~~jP2K{;9T!C!J zM{Cjde9q{S+hSz;&n69oMo!pib`{`l4_B{+;CPDL5%v1$xX%bxbIQtL>}ur@B6y$( zcudjGwr~eikW8pi1vbL+vEd~5o1aW8a$>64gPX%ug#++4q+MVzd_<_7h}>2oh(PUh zU4Vt&NSD?Y>y_TL2@(kOz2GOOmGp!SU~!9=$Z<1t4IG>oegS&N-FE!a-la=1j-XB2 z4uXEjha4r2q=ZljUS*cqI5)IL5r`rahj-I=(D*EkOt9 zvqf>!go|Y4kKm)NF#WS0grOMXzF1(agP78iO+W^j%D#vc|4Wd=%mS1W4AX&8Oio7D zhx<-q6+!q7F1}J}o1+Lm5w{)=67;q$W!ixXpq!4`OpIP`2ZQ2z4-5@t+ll}s;wi-V z1`)yPE+Km08xlR-)3fd&5YjS#yG0=dV?~@G7P~RbWHnfM4PhWr9~p(%+_La72Sa|{$#4tyXU3-eN20Y|q0oj(h?^n@B$ zR&!?CCtqUNUW%`gLq=FZ<1?`A5CAx~L|@&ylSLpcmJ@>-4y+DpHUYBG|fVT_|Y*B=oU+az1ut?K>Lv7n%A2 zU$)YW9CoNj%hq~)p&a(&*G`~ET|cnnjb!)@7iYLG-^;32vZipbp%O{by&V^ZT^L~R zzv^S9%F@pFbXdC6V(*0pf%$t2UnG1FE8($Usgl<P2+XYFAXCOktf>@mR<_T@vMC5y=paLph`bh%lpAXW?(X8y zJr&&x7QKujfe4A`(_{cM4j3znf@H$G3Je9M(bjYGWedQu2$)e5yr3Gb$%^C!D%`SRjojD-jaE`oF?70nqk1Y$Qo50Zlt=2%Wp3*rNa3ZdkH z{7sl&Rbj0&8xx7giC=L-gH=ezlA0Q@@EHhE0Iz>n1%V_G@L_+4sF8R5{RyERN7EXQ zeQ=%4V0R0mZ~%jRS5zuP+ql7Rh+QHr$yVG+5Q{-I5qm}Ni|L1nNx_5!2$<6V_LTg) zdTc#>mYvD9^u&0y(O42;1;&6-@F>oW0Jvrr?7rSsChFyYs70@ zSdNkNH7L))<;!<`*dyy6_AsVbmn2&;q+_PMb&I0kRg~t2{gPJsNj1(dIBs1o6)dY*-yKY45UDWuJ(yAYCrU2{NS!OAbe$=hEJQ22a1?>mMrb{_2+G);hUD4#bPnR%(| zH+cW_^yR&hy%@e}(N~FEzY~o5lC^iZ^y-%28RAnk`Sbsx3ee5@by}og?ZvI94u+nd zv8+S|x^NztCZS5I^lX>0<1gIMiNfv_HK2qP0hamdDmM-Tr-?ym_ehvnuo9K@(j z9>WDh2xJ02W6_is3-52wH>pw{akVkPF3P3pgoFn$4H=BZh)euQAj}PG4^W_%Sb82F9W`T^$u+@q9&t)Dfs-`+8i019l%67$X>d4Co><0-Rt!Gh_K* zIaNSTyrlzRK^)-hqlE0aVnTv#aw@UIcAA?VPgK?M$Pjk`;sQHjp>gb|Ac#ezBP5Ax z*3J8(LqVbUFn@&+F=mE^>;LE5>Rx8#H!jd>B>;(n0+}mpMDu1OSc|8I-+=PCu^v&h zpAvTyOCNs}kzn}qcE0HAP4yyjr=Y|dplI1+{u zHYuv)YIDpI=HOwaRLBVFekQr5Rub#*DSuqB)NPX=fG`wHnqX`y2ceJwHB9Ws=ckF@ zV`}(@gy9{np|qAHO%06WR!l!fs{=qVg)?P|2V&4$XZPR%=(jmdXKhb?oAj%O$Y^qP zbDYZXAs{z*Z3Za$w>sVvm&(b7Y9=~}+B3vkw#}l@7o=ds$^mO;hL0^lW13zVLYyzK z7B5_SG5=0%E2r4Ioc+f$kv$x$1N@M{U`yb7lEICm1V4KEjj|3n9bjn^Z!B(CVmP|s z4}iWNP~kFLoj{YP9gM_BGS{KBg`h{si7x{^EKIWJIsuR7?|M@x6o(=$3;3g8q?!Uz zKsC64MVI-5=#&EHM@jlazVql%T+kv6sT*OgoJ`?H6f!`mT#QC200%M#tbtX=fZ-DT z^W}PS-J|LwHdMVDkYGm|P{Us~pk{CV&@8_Z7E>>00>I|WCpkPi@?Kk@g)w}J8%q7U zK{{8a$9P@WKd<<6nikW_@O02!vD8M8{{mb*Zry~5T|`A{fnE@Zb97lVhbyY!-GWZ0 zixGA8^-b@Bp?6~ax4ii<%9`hN-#dn4?G$b2bfhi6=_g4jUXiUrcp7! zN~sLuMh&Ki-Hk6oN`vPhmv!vU9Vu|!oEE5WEz2_wHQ3p`FahaeQedYo{yG*f{jeyE z9{FE*`nw?H(E=nS8~Vr9#jdt&9zYL;%DXF2rvFM(St-LaHE>@opd@ zaw&#TPs3w}1N)H|+~>)7?KPJo5MmZU!@(p5#x z@r7;AQmxx=vkMzM*g+&rMyUGC^#`_0RjE8|6a(P4rTBi5tic9nn$^Y?*LI}NPT&rF zn9o@?UNBZQ&kSG`z&Q1ZZb}e2MkXVb@pkY8P{M4@;5#NA>RI_s5J4jx`zlKzE+o8Q zYB8JJJ9f*m=%hrNgg8a$2}W#>gSY5GwX)V^MtTIV5NgTLu@3RFn_jcU?LI>lwYri$ z^SO^bVxDyhyPK{e(`E#WJ#FcT`1}>A2Too99!RpK($Z{zZJZ#BJ!8ru4>#CSDGs6U z9!pH}dkr&2#m*BmA`#F4O6bK`WmI~tb%e=wf6vq|mobG#Pp(j0;Zd+*W~^(J;j?DD z3god)PkD^sXm$BJJA768HNhUDp{w8ko-NA=y=Yp5a)}*?fc(K~+MkmxDme715K%~H zeMHjNDDcBZ&_#q}B9*Yh*1tE`g!V&-un^&J#5sVH2taUiUI(P_>mDsXz{6{pfso0h zQh^(vkvVrwPEBnA|Ks9T#6vxl5oe=`E@Fs}Ho>(u092HGx*olJaWHPg!!~p38=ujx z#KHg-f#M9&kpK`>;i`_h`ff=CuH(AV`ZP%JowXcvB_t3~eJvOQoG>Mb!844O?X|j0 zf0viS z(}uvqYaaxu5h6B_I4gM;yD%@CB?ttkIaPxaqmFMXoL4e4M`kI1`8xSbaUaRkm@Xaa zdygZ&;53n5WD@0&Tr|}1rPkUgCg6Rt4O?TRMF@gCHvtIL&-Mv%AbZ>$Oj zLJ=zndkupya#9|yY*QbibVFll8&1?666`e+L@}5JjwE6biBsr0Cod6pKMqiN<4xl1 zfl)*)wX-W-_$v$*<7_JRK1#wt zjH=Q~J0Iovk)e=qOz`rYAhj_52!l*WnU~$Dz^D709Fmz+^8vY~c#*nfy0HZ|)coOxw!!#&V zsmgXLtt}yt&@??|UhA>;_%S~`IVi$7wwTI=cxi}X30b9Fv`M5kRt`=Fy%>e*R-0ZJ zd|FVO90-Fz#Hyt{kPQWuI}JM%^*_l>Kgm%6=Kq#Sie`!nz$ls;HTweDp0)bvo+zbn zYMZv|-X(aEm^VOsO79YnGlR*xn2P}!1(UsMaHPM&?>Gg4Fr2Jx?g1Vt=*gHu(RPp;v=^aKX)tCm)*%aVYRE>&Lk%f|F9H|Xr7mIw zGA7hPK!U{fSuz7p`^P{=P07V3Fc(0*% zdba@u_}?8FY9;jKKT@XD z6ywQsWuQu;TY#n$!c7}EW3=FM0O(85bM)8E;k_9}g$?O~lq4>!d2ixkdv6JIR_7pO zLdpZ;cEpVw0-|b3aJy;L&RHSAiK)4-&ztdLR2BZ$LzW7L_409f6=ShF5S$_eL@`Gmt_tsALyS4)Nt~X~l(QBA!zl;sYa)j&9472KzLxsb^#V{c%mhev048(|#_-u4KmGct zD1|P~q%yD-{w6`<-5@-=kg>B&Sn5q%0=tuFIrWnZ4(k&#Luzn2)_`*5rDy*Z8eUPf zt^t1%3&j7iCB*iixE}(4W6H~vk6yb76J9hU?h9(CXX1x)LLiF&K{p&Eryme(5Ttkq z-9C9VvMrO`fYgO@5Sic(ArUq}D*_?`aAc_j_Qk`UkfcMNA7}s)_D?h+ZUtUgf$7lX zD&Ok>QvR7rb1}0B6$Q|+4oL100z3p|9qVvuXyXIsO9@ntD;JKSOm>Ln2KL_y;HgC;yY+r*cKxa^ zu=fjLSPn;VHv9T;?aDZ)hh;hLndgilR>gBWf+I08Sgh=xIV>|Pg$uJ{gGSv;_*rLa z913DN{IdQk92Erw116^d72=#}queAxU*alUu&S=XVd+|KK|sQ_C(hhc%RN)F4ycCV za1BcU+EZl6ws86g4(@Ox5Ri%~aDvRk>G?lM{OV|c}-Z>%>gw&26hyQg*|)_qoxekb5K1p#BQWE7zL1YInC6}r`U zv*P?dCo<#DVKl<6&^-bf6%!079Uc5e#zbr&ks-Wj zrHU_*AS18`*PWjc5`lNq$mp^Eu6z zXlUV9awsT|=Ljb>QTru>byLm}Kmi_b5^nYkcLzh|>lcX)m!aOx0U9je#`i7% z9&6lx|KnfupeULkZrh{|4Qmy+?E2BOxIG=%0T>J#COAs$2XJ}dYpWoSZOV%RO9@c0 ze4?lV^mQ60J6{fpbZfYWSJn{K$Bt)3P*!B*6V=nVEe(Ku5?H&Ub{fI`06RQ3SDAE>rgC; z7+IhRmVy45N_lmZRGKCr37{9hg-mvL0s`3oB^_yJ?D7qot5{;LV)Nk>PwJ9wU`ZkX zg0UQfQbU5S1tR0`L)jO0=Ts}_Ve1F#QCCTt;EXJg3ZfCg(iWfFfN?n=MDBIyf&l+Z zT@FO~9sow*Al0rFGAt%BsdyFd{3y(TPu^H7?&{&-p2pP90XT4&S8olOcpwyDGcaYc zJu|y34?q}0?x-jr0`fG71AmhHAP;u5vs0!Ff+InXC_!UT!-#!?@E;kl55O=oN+-d< zk-xTF3E|-dr077zx};bg$Xp9I<_N;M<+iElP=jYax3a0Uz60?Optg-Q;JMn7r)Xbt z6(>*vd90D47W0(ZMHV71pymIF$6}rY;3Rf&Tuu+9h*PL$LWs4*$U7>dYjQa$2yCqE z5Qs1ez<&u)W_2r>onu?xfDmbP;i0Wf-+9n2?F{@=^-K^>R)Bo!XI=xJ5rVv1N=<67 z{N?AE+S1{JDHZ6pB6!(CUQ@v^fN=Fpx9=)$-4HLP>prctcoiJC*wD7|*N5US9?j+gm;uBg2cJTf>S|E`(WL z1N=EXNq9}tfpk2g*gm)!AW?fP+QLv*_?#@PIyhOpfb@6?;XD`+-G_QGl?x|(31Wo? z9#z;mRTfg}JM36c%5WGD{&UU=Q!*bm^K5@0Z%P1ZL_pw1=wOY^zLOsI*V&TPTs{z< zps=%e9D@#pf{juDm_%r+Vm2RPICMf?eT_??pka`i*2_S^6G94Q5S>Slr%ZQQ6!hp# z!*m#SJKUF;b8&*MA_rNX>e~duydM>>5(*UuT40c)Ym2}?T{KA8V)CWRYl_u*WeVMG z)cRN>MsK&okELCKqE6OHaTRN95zL*#;w%l}aD+DbEs8hqQ}Pd!og&f3U@L{3M+`g) z7lcLjr7F8M=caba?*`kXjetFFZWWbV0w2nd5t6>Y$-Q(8Equ>j&Fg<$D(e~08WhVY7MxG=`FU+E>2_%k~ zC-jZsUY+FBUTS7lC%49?0A$>(+NeqP0D%AZG$I2hG|FsG@>0!AN8kW<2?fiN7i|;B zQWy6=UIRJnlKfsKqp29rm5}7pmah^m^>KT)qlOe24G=sO^@q>Mq)63U6*El(+#RamvoJfc7nk)*16PX!RpMOBH#H zlE9`f!htq`+m@#Z%jCV2uBq+2QpjXgK}oIqHr%#3c!` z34&_d5#AmJUY5|+m$WSu);%Sup=1SrF}M(P%7#6$Sy~_xD-)rFo+=@Jv1Ox^qQsOk zB@K1Tc(6qYfzQ=UkIfa4tbz}^#V8231}7}V&l<%p;}i!dKx=MgiyWs=+6%>??l6$^ z)Spd{g3R%jD8)gSbb$jsG7tx$4PTZ_Xlw%svM(1>QfIRgX;%EPjkvop&Z^09{%5!@ zMTAg_^k*@?Ps`S%{S9s4!cTb=1X!^aeenL44ejdKC>q6)Vgd<_*xAh)Yc2@Vtk zf`$i=aO~tpf(hm`;nd+LIDueM$Y>ZCct_z3=)nFe+~5v8wLo&)4d@U!?mrJ&<3Jf6 z+x9XWSp=4qb`a_ zC_4w4jx5+n1v`n^wJFL*>}Cae!i*M4VV8e(4MQc!PST9z8ycDbJ|yZz3s8&DV8lQ( z9$nXqxWT(Gsg;93B3g>QP|6h;8e-01$>d2J2rSvX`!zs*hmWViq4^njlm*XExGXa` zJB|0($h*Z+@;sG=Dv?hXZg3c{nXuyjtN7%7FCgX&BYAOX*`4CPUd{#NQ|hRr&ao|3 zCdUP)7B_W>h;s2%QywV)$U(QxQSOEfuro^W$~1F4u;IGERQF*EMU__;k-^DcRGx;S z4~7lLL1_5##FRP}h}gnOk~@eaz-?p%!d6lEFX1z*$_T;a$h$p)#~!-i8_Zn8SwxS( ze^~9Ji)QaB>`e@Wz1uPQ9o*As7qJ%Db`?Q>>TQ961_cQP>g(1T^AJQ0M?TRh;fm35 z!ph0MBo-E{whTrwqu@$(U=2_MaKh3kG-G(j0-(?v`By?m>D4-cET8AMa2PHCzYbvx zJ0l3q7n}-%=QG9oy@PGt>z4~wQcOqeo^lvqAc360Qk3EflF$1n&Zk0DP<%`J(} zfWp27PGK91mr-Qg3T%CMYsaVX*V4;_tf!(u=FD`LGhfSnkdOHA0KOme7F&|jn3Pqc zFU{mwfN?xhr&TiuRx%WTMg?|bu2?h-c)L;MKiYx1jfCFakc?O+exl)9L?xb5vlGHK zeMep(Ysm*bfkq@y0jxqMh`}F0aDLf6wVBaw?Sh3hnd0$Khafc;&0?f|C3kkU1?K85j+PhJ~F(uz1V7A7BFAxB>*Y zXHoy6f#}UlSGq?y|88VGYcUolZXoEiXhji=ucDP)!~=M_ZP)}21)`o+7y!G&Rn4^S zv@8Ig#7Y+;Nn6urN$~(ZW*&)qlSSw@lM?2LuRgoqlD67iEV5NH$ex4%0v@+Bax{U1 zl_8VWZR&LkUyp6$6@;mfJcI62wU!ly>9tOhE# zP^$`&HHk@7$|+6rJ^ReEYmH+K;{vLv3YRp(cDzsre79E^&Ukn!3?#RSY3oA?sdek* zo-cy@d_&Mk5Tzp${jWo%NVMuI6rD>9yiArhCD4sD2?bqTJ1HRLcf<3@ZPOV3SYIAP zO#9?*05ytlsQSDobuQ0>_TJ17jAc0wC0wHx70=fShCuZ~ECuOlACY5PY}`MhD%vnp zODUA*mZtK!tQH14j13-_dU9y$JQY)GEwh9#F@L;%&>U`_V|%C@dz885DkFA%bw<|G zR?xb&EEo&=9{Cz+Yy}!leLV-B?Rkq_EQ~0hzi9X}x08e&VHLG7`B~$JRWTJ)iji2} zO_bGe?h3JdIZ=<+7A_(~@4!BXEg+1T>}CY9nl&|L9m#gS|}*7 z;t3s0ASVY950t}3zz5tW=5gz3&?KVPV1E>G@ibI2bcrD(J_CRkc96)_Gl_sF-6t}3 zyiwZ44l6SioI03Eo5zWepRoqS^2)!5w^er;mq5i z;f1`s1_B7yMUS=E(JqEWG^G|m1~{5|7VAooMtbCO4RiTtu=S%1LkAE7)EBYn;}pAU zUYvaSq8)I=qvr?zHvudenJBXuZEhJ&1Nfvl$7zDtQtuN7iZLFnKeSrqtc4J$)Dh+u z0D(7}{F=1OSt}Mn>848sjz#NvnS1KlCE8BQF%~}H?#_o_!j6P^^atX80Wu-z4rJB` zJmXPo>IVX#z|14EDUJT1pq1Vk5rCXeFh~WI-fuV3g@vGM#10r4x)Z6bkazq~K0{IR z>A3VWR6SLj7mytn0qyuGJyV<~bLRESG^Sof?0z+32_NXkr!fMR^l3gD z80x?HEb}{B)vkzPI#u*ZW2_7r2%QGmtUl~qUI4F#+hXV!V6#FQR@bURPH1~)F+~f` zQODi^T>39#+|H>eIL))*MT)-@-lqZGOe1=Wi^ce$kq=J|S%qaOAsCTd<#-HHLF&5( ztK?MoO4Pn>=qQ>RRPypB$L?FS1w-NMG?vKuGt6V(wp_BeihYo%^mXh(z>1=ezcu;zM zD6X`#e4CBZnkfRyk=}S{7ilD=P?50|B0~@UP_99Uh+f9E73x2`%G& zeNwf>0${j`dysPdNpO-3t!ZWEa{_||hao1`q0t{vF*ybm@u+c8k`*LD7s86V7DPYb z5M&h5P^zrua&{un0%8(-hV*cblJiLpyYZ0yTPp?!Yf=Iju#})CauXsut|AAL zbntABb$NSc!BGW0V3xfg<-!$kf)p#pKOMUnWrLy!5LOGl*fqSVS!h$$2AT27D*DR= z0TETkNWJS;ozG!o2!@RMDS-@y#kwC;{YijV98tIG=ZT`BW{i6l0VYzodILvOW&%4~ z^h+P>l&lx$rMk~zeg=U9pNR=7EYu7I0xf(#{E$m<6xZZLv=&Y-l z!EIs#%;a``+S4o1;cRVC4r!eUT%}G+GO7txl}(8qyr?+bxludqq92H|<%V@y;#PTL zTipo~N&_$>StS7%w3-28;_273Ni`Qf ztAbKB&zz#phEV|nAVT#sbbyU%*i+vxk+3)F2xTcNSbK?M#3}5?Olteh5(*C+>6GN^ zd^FM9rmN5z*Lv)}V8X;(;Fy(HNoXiJ<5#@}z;8cOaSBj`uJn|_jg5#b9~J!E6`K33 zpgf2&Baod3jk$fL_`*`s#>WdG@oW)TNc0Rd1a>DRMjkR1Y!L(CM|5h;Lr&3;-1?r^ zn9+&D5J_MMU?I8(n**lcK)>xT2%!V05Am~{*UIpZ;01b~kp(m0+T_};5di6F27G@4 zV6WXX#Ww!!BLYy25jh6$4JzAVM`PXCnYE;}9oHd{vXmr`??6~;Aran>IT{)8QNdV8 zoWW-mfVP1iYcho!3$96yg$s)DY2`M{fNdWHDU{NKyNO6>gsoFy>yQLcfn=h~gw;$! zh%F!vGlVucA#2ppHAEqxL>5EI^U2Xg6!?j_8!%okqVE&RMLE`B%o5oU-w71aGIS>0 zBWfVFSulZg0H3Df^Tx;wBE1g{*0V@px1`87yT;=zqaW za6@paj2wv9Zg>#2Qhpd9CxIr+e|#t!LD*JJIdec*odbrNuTR!2jhXTTpo8B~WtYw* zlav8EFW}mG>*sh-(6qzTke`A9&9RTWekK(X^=PkCcSnReEs1M8DbO4Q^wL7&R4ZnVS$!aDL#*&p`4N4wWwIYyOFOAy+@ClhIG5fmW zxU+FlDJQ3L=5Fx{VSXdW_?In&zz}TL_k*uUlc%COI0M&j@5+cFu0vtJd%!eIMDZML zii&quK5}e*QHi`DsQ5#4nxK^XsI)CV49wumTkG_9uGq4(C-){d5O;xzjK>;s)-m#x?2z&`JU$)U+W}IorIP zK!`d7c!cjIV+*B;bi4SUz%BlNF|oHT7(`{#^+LTBgTEsW&l=LiK7sq67t{}H2Zp2K z>l@?zOg={8rRvIL&G!^eEO@EV840`5-k+gHc}ELkh10eu0FD7$0OvBU! zGWwPa!7}6rNg_S}{qT!qzZWdmO3WaFg1NcWh&`57XW(!mAmdUXReZ3Lbdz1=`$z7| z&gIaZJ56vnH!%km5B059U(i9sI!}R@(obsj87DU$rd8A8S%-2E0{_1{R2!7`D?BT# z<~|??t)gqF&^esPrU{}MFe-hMdb^_;=PM}3343@BRFAZGPM>I{iQV5Vz^WfJQK}l+ z2q60)08Ri%1gdOtn8W3h1a60}pBq5VfZ|xet98Iga3}H9R$)>2X%#v!{E4D)6}7Ax z4I3tg>vs~yC8(Pw2?%|O82+gAf`Hx~dR!*R@9yg0SguEVw?dMZw^&}$HCPy;H|JooZ8~dpuK4D5gcMv)Z{2V z+9sMy?p*I9Ix*niNaN49x}?z)Eyyk-w{{T9hmq2>}{1E_2aeNlVwc+q^ws6Vn~NG+$rR#6LJ zyI`Nk0RiVw?z&xU9mHS37^QNi7!Si9Fwl5Ff^P=L^w=L`Z;3yD8uk}@4emlx==^8U zU^1#h&C?J^BC~iH;ZR~+Wa&*}a|PA9|JDa3pRcXgZ|tbUQKI}yhd4F4WLx)?&oC;F z9OS_|x7$nwfs4bg2^ym0ZjiqBGU!k@hsn z;x-FYEKpEx7)^wt`(uCcfF$8fAMx2Af&8dQLPxK(wz1f*CnUU#? z>?SS(UF6u*5zC#Csx3~MGaKn9^-{4EW3iTLGA`ID*Eyt-)V7&kp8XS?(PY{+QN4Xq28VZE5z@3f^F%u?b zC>FoP0_kb#@9KmPW17I)%|9UEGSM^wmrYtAe7gQEcaHI>dMOdfN6qFndlhIjBwtMeiN4knv zV;D)M0OONEpxP29!IPhOW+Lv>zpVnx;9=zF^S)W3zouUy65pn5|2Qj%koB` z4KeJz6dNsYlSrYy78908?=AO5g$nSO6Wy8NG3-E z4;qI*tI6@|p<7ey*Gi2V4KmfZQ5@`6Gva0Zofi###CC-d{20my$>2sYqu&=#52Gs* zzG1F%wr467V1@2fL`l-^DT7XdAVRPNp0 zyO(CY7?^|&bAOz!^u zaQe~KtbXGhXFJ6%p9%D~k4bNI640?YgIqM8oFNlx>OnDv@~Dvn*dhm43v5Yca5n6M zi3p=~vO>9f4XWW$)Jii*$QOf9D^YGz9?})v;`UX*lV17^H#9s$_=QsXo^Z^387o#- zaX6-9#4lw&f-g;k*8|GxkHB0t?sTW=v#9h~Qeah?&32f{HfaPn`lWOxCE5;X$s{sU41RL|BCadwtZb{X9eo;|BsV$(J9c_vPu+= zg?8N|3P7Y7&w?gk*=YsIw3~w96$toyNUU%%>w~$(Zot6l!OjT3g7d>Scp3q|5s92j zhrEf?LWJ(@UF7f6G00d8FQsFqmp>iIstIS!$xS+tKbERNhDt7)nxgu+_#IY*)uSbE zffWsP#}5)59VfdDuh8$suw}a( z+6IL*U|(st16I!$1OS-8MC@;pFia1mUw}s!u&}Pu(mn)6z>v*q@{QlHKsp}5#uu;- z2Tj)#o7XN5%mc-k9p}PL?w}toG@x;0{oqORA0Pf3`5T}Q;f(6iB~Ae@32N|Sg7{H5 zVcHN?crXoMVTRE6iiEW_6z;`c9`4uUfVKeKbP2`y2|Ae%H0!Xb zbBoDNl2r?LmDov*jer}hPpMcE@UT3zx$)s0nl+U>dQ~lGN&hJy0W!uJ0G&4={qP3T z>NyRS9Bd^zfNcjvJRXXz9Kh;PHE`KwNEU@8&_aY?frum5b_&dE1j?I2dr&4JF3O%` zK|FA3*3{6WkH`FlUY9D~#mAaBrS}uH!gh(5Ff~|u=;Z6l6k#Un{GUKCl%t)}xx`7j zAFPu^`YY+lBblO-J{s$OVhm`ZwP`q6y(S-fkZ?2}9%dw2Osl?(hUh#=oT|+{EYNL6 z%u4XQzVo`%Yz2ma%N{Qm?9@PNunSp83qbQ#<}Vkx69uE*W#_AE%Sd$qwwJp=+lZ8#mQ%n z)scOKl)i8aRjgAvL_TSx1x9zW;EE;7P34%hhB)2NY0 zRb5$?gll}Yx_i>^y;n+>4!S@bXidE=??VHlZjAQU-i`Mbfe+P_0plUTssxP#6Qi*@ zv5oj=iFh(0W5o5YH(|g^^vGe$AZ>PW3Jyv=q^@+dt3pgmfDDc(0`WLu z>dZwqqPr7?cqOJmXs_7QY}Lp2JB9z14U1JTOn1INaM)%I>06c zQ3$H#$yO#75=2oXilRL6ah+US2B%}z?A6EE)V&*r@@3G*o3nXZA zt*~yBvqF2(0?PN;K>B49fAn!68jp*H~g#z0T4uE%4NYq(}#s5i%N`B!fu?q@MxK zRY%8*uE=4xRaHak0*lzLc6r}VCW)AC_88jMYljBak-Md#KI zGse$`6$0>p!RZGc9w02fO%fPlAockXlno$5LXHEB2qD-h5%535k=<#BfR2f6=YdeE zdxCNB3*p*67;q9vo9pT1(5yPIOJ1&g^~%>2E#Bf(N*+zCUTZ7H>;@ymgn+%=F_dqy z$2!dT*5Q=1W}Dp>z3VKDBvC)wX8`Y_L4d zvfQQ54|PV875!#rLuO^pJL+BiE3|9aoMi+k2>8$C{PHg3NPk+y^|{B72SMC88vs5A z(SMDN<>5rwyVytz))B6dv3>QhrsLD3)v7t=Wq~ctn9Yo+DYm*9L{V`@&0(?CiGbO! zBVI5>O?X{%akETH7P01u7-+Xcm=L9V%Lv}pp?n5~fbp#&`9j~}1(KD0J|qlwW`v?O zTA?Z744zv73`*cxU(6B=^s-^8PZlAG=efZ^2Baww)tLk_=VHlsbPX#J`XYWuMbRI- z6<7$}0($fj4(B7Kpe`b9?Fg(Mgcbgb#uo~1sON(dX*JSHeljK26w8fUB{bY#6DT#I zA>8ch6aml(@?q+S^38}ui_Q2Y-k=gcra*~kMA&m}&r*fg30V$kQS86pF78=oDW6w2>_G;!Mn%lxnEJ5w}O{K4L0l$W#k z@W-;U`5QIdmFU9yo;(_O$iHm+EN(}tYAA`chy)w4=LiRmj{t3Y=UVmVn5ecZuHUZR z(QYPATjqH;rTg2&r%4t?|0&wIW!7OLf2* z2+lvjHo^yxKxN4_b3Gu)a0Zvao1`@vUTBT#vAEwxtvT7C3Xd{`4hj3iL{f#O&1I#S=+tZQvB4*Dk2sWLdvl ze?zE23*Jovta=p}k~yVE-(rFou_z=3Z&T<&Gw6yrdb6rop9_Y_ifAc0qFFLNPIX^s zzK6QPSA*6hl7MSMwkGgB5D)jL2f9<%tuTtrMK0c0V6Ick+cUk7h)h=Hrr)oH7fp!b4+=F1U5wvHv_bHuAruAc8087B%>W%5$>jy zB04SB7-NUcEs{M%?tR?iNgyEgJBCAHgDhWBR7X|Ps6x)Oyp~_|4zUs~>y8uxmn`jW zQQ()59#<$i25CYKZ$QYB$a?88nxaG;%|ko5WnH9i;EiB}TJCxvAZ1>ZgBMUzc9>d> z7xx^4r!s-|9eCi-EFm{aY$@2-l^nWZ!+2riKKd?NNO&oR_>4i^gg})erUTQ3XA!L1 zx`FU+x=Vw|qqYmyNC4<)U7DIj=TviUTD#swo>p+cAs8xEKT=Z4q_kj6-eC>#~c zE`o!bMbcyNUHQ#X6N3HE3}-QAl`m#NEQ%T)O}6hfi;qUtqu5?{M$R4gQ9p20m@T|> z=_#)fQ|i`ZOpJcej}7khhf63Iw%s_;e-d3EwedM4QI3%;qKtCQPU-a&f{YFUgA3=@ zVF+qrPn-4uiL?PBdi{~+-*g8309y*8K9tCK%SN|#G8@<3ew?%ngrg$44>j?W2rYH? z*&-pPS}{;F88Jl7u-?;BK@mHN1kg(eKKYIS(fP6xs6Zs zt>n%jQVxo5x6C+dTt$1(ai}PBqa}x8pQzWw2~xWQN^p^;EZa()JHNQ)myoNgx;}JV?+HmXNTF8OPb$h z=p@|Xwf+WzZ(6CQyHGdkIvGnM0x8g)zVax%F~rCwQQnox%&00xj*eY2Eg)2oq2y6l z3&sd~Py$q622L@7jgqK`V`48vB1F&uU>lRd*Z~k{&x1m43cjxJm8&<|Ch~pU( zb%#iX1qo>dr*#nY^~S+VOv_OMoIQQ-4f`5)d0h=r=``1@XUK*f?^bbG6ADb2ixZi` z#M(d6m_tg1s?dm2L<62XqA>@8S|Evp)-3e@unmANCb2{I8&tY}^&DHaDtWKLMpGat0-8|G874$Jm?y;Qhpa6cI29cG~>rVtbJV;HX| zqv9(hnGSe}o*n#r4wPkJ87ALA^laC5uUa*baKnoIdUtS9xp-+<0Y zAVm%09`ODKzzx7naQUVgP&cD9 z;>ThfL|NoZZ$TP#YLN&a;<28}eeNzMfNuC2JX`}s|K*Zmyab<}%3g9amxGILJ=M?n zLXvh$nGTKvO)-V>F(VL}ksy5;6Ph)d@JYbkIipvCU@C&t z2Z%~76al!bo;%4m5=XMBP);0JH*T~#s)a%Tfhe8XLy{5)Lo5?lcW6Yh1T4yCH+Z@+ z<0m%OeOj@I(*vvanegagR0$CggRKga5=M5JP6JI3JI;ZT^TD~U&ae(03ryg++C~UF zo6M}XArGlfE~;r<(2l7$(_OC|!A+{*^1VPX^ z>Ug^umk0FH{KSH3L$bxh|N8NI;q+Q)rlck}gtCZB#H9`N5EgZAx{)25*8Q&_)eirbzz(LcujA znobP0H?g6G(Llbz^sqWq!q9{%YhikGb?B)vH#8E(^$uWA36e4s3NVT-?}@{a|I|lp zu*zH^g@j`FKLZO)zMm50TqTrQ5%^Gp1Y4YUFT(iFonfqyZRP0gzOY4HaYL zDDVZ@#KJUyKR|sr|1S}3e<8w!hKM^rn}kD?GI3Dxm?Du@NfLr(1^KAhhb=z8ZU>uM zn;bmY7w`o@rZE3yrFZ!dVNxWTBmv11K^=D^LP~JH4-jjUOo_yC8Hw9?q!CcGEOdIN zwTw?8YxUy~bFR%q5KflwW5;+6+Dch%NMtIa7)7c!gh1z7eS=R0cr~OW ztfp4lfv1YiOK376CCb16?NxY!z6$0nOwSM7)6Cj5#4u@4j2!wu?mBxDH5BVoFfvVkL99avWnSij@Q3S zVQcc|wUSO1ej+~|o&khD6esIK$wO>Sh`oAAc*3b(C ziDY0e?s$!cWJ9^wcRL&Nyg}JbrW>=B|Cb7)SWRGTM-~i)zg6xjO28~d1 zIS*I%RVd%qW0RSSAoFS!9=H!2P|pTWqQIC7PcEZ6gm^^;IS?T*pjhxRIglnG1*NtW zltd*)@YygF#OU;iv1gG$uLveDqEfuB21EQA)0mx9B0dYnwpuLMige+6cb+uVfRVhz zAM((^NQR-c2?@YcAJMvo#6rZ6h^6}*&`%(heDHLWvsvv_@T2HY3HGn1bgAuhS!eVM zB9iaosPk%T7AsAAIn8WCjL_H^Z_plaHbjKpiuM{#9&6L)e#v-nac^WEV4!8t*g0No z!#NwBT6-OFd%=V_Ra)Hq_HeBurVf+(pD$QWk*=dbdw=dr1J~$;Doku9nUcSF+6v!l z3v3rtNLQ}}wZ}uMyeXnTO8v^i!z-{UJtPLPj~O|kUDEA*xEh@fjUZ= z%<+p{fs1ohqJA~s2~K1-=GWJU^OQNt+s1k5=4nBG6*c(0O29KJnv{qYHUYcU??iHCrJq(tDb^F631615`rF46tdxH2^Ii7Nq5ff(qZY0 z%OjMe@OYG!`;mM$5O@$;5Vc!vG z1WC$j6wH}+Ne-=1bHb$IvyIKQkTw3Q+5|lcN4}mPg6AV4gZW4F47ubOr_Z#wDb&%}Z#4&YQV499-3?#lOq7^oay%*7E!hx)Ok#0$inTwayq}<1NQU z20D>C9a&)ht}JW_;Mpchmfe}jNF&h7xPpzdd^LfbxcXiXQm(E(7JIR4bOo(M{YI32}J> zLb3xa0sBKnQ32vqIF86kK>(NHv6^c?zd`Pg-4oxUgV2CB-43Wk%DL9ZLVFAl<(x>c zFG%vbObmHlr$XP3| zQm*;qq$j(DLNvhUm{98+BgM4HEGkQcvU{O4rU{2Und+DrJPd;saOTc8;?p{ znzvX+_XH)NUA;y&Jh1H;D7;ld)3nR?0x9Z488GpTQ__!FgMBYagmjmD>W&m$1Wreu zTpluV0~E+v2T)ERfews*ko7)dM)5m$$E#}dG^kF}0BQQ*Lwuo6s zx{*B<5yV2jE*aW|Vlgr5!ke3&`VnX}j*g=%2F|~M^R9~p?hPhLb4;S5(x`Ld@7^_HE5A_(ChLLH7FC#PPq++Z!$>`I?{qzB$!&L|3(}j zb)f^KCT7>OJ`KBZ&|HcOun8O+d`Xm$-R;spoUiw{-(GyS=|C7K9jR!NJy_J+5|L^` zCSsS?BTN|!S@|%28W0}=unXc0!^qLI1~Glf=z#%)Oz-?$N&2YI_evAx@(Las{~O^> zzDM4BANv)Lk0sPL2;hL~FOC1W?NC-hcjBTI#No&AsWw_f$P5~PvJdC~S<^vs0mV=EvcmO#zbASNL z7A)$hE#vyp{JTd}Me29r#EX_-Uvw?rFPHe|3L|^`Oy&dC5b~Q2|Y%8u=@G{)K?ne0{l}WFay-=7 zwFwQ@Ey3Xg;!tpHU|#Jz z;>w$jD>re1n%F}JcJ$B~qAU#0VX4>)w1Cdw6asK^a^l>>eQcn(Fh|ND(STmzdTVq5 z`eK-s_IURUe;jpAU)PO>APra|f6jBC`}V7*RvU(U_xNC8aF%IvHc#KfO7q1YJ~51! zdoBxN8p>Ya$PKuCe29Cuhvb_Aren-69Fbx%aDm3lXiE|_KY?O%KiMZssROC#rp$8S zf(jcIeXZM_s#r#~g{=xZY zy~E1rYGf^ysvU{Iac`9%0UZ}@D#I`CX)ILt1^Pgb_A;9DTl)HK=D0NvCcBrHi5r^h zU)_~#uj*Om@p_4+XhuEl?uCc!`^t7@!R_|CWnZ1d^fB@*yI>d7IMy-m3+t>)C^vfe zZTe2m8XM^dPMr(2C82JZ+6~lMUpu^`fR3~ph1ZjUK} zN^-VXQv?!`D7EomKnyH{Z%y9G`SFVi$qo!)ojo{I2KjNlL7B#WDB-4<uOuF zlQy=NPr8bAJjRBzlP%S^NFx(B9_j_Qo2@tWZh(viKQFI8yfXf!aCkW;cj);z>GA?; zpF?_!W>1wM<`Q%PlXd1>o77tf3DymhY|G~xG!##UiOEpp`%pnaSuUDw^Lh zl4P{>6B%dCmYKh0UQIc4M2eOW8LqWytMI~$jO4S1oXF1f+0iM=hS&C%6iL(Rt5X`}_S!W5KMr4=;vVfzX z_EpiA_gPZfR)VvIf=kD&8eL&&y356osAajBe-{r8d%9W?&GZIVlFHTj8P_9K<6(v- z2jO6576M>wJJDOM=+)hfEieLY5k4ssk$IN?3Dh6|Z9YySArT`m589y%LodJt6Xwp; zBxOOpZdMjf;ex31QI4@D>UIa6TcRnzt$~AyLdj6TC}3NIOmtGf^z?>i0wGV(#YI9b ziqKSKMC!jPrk{T7;&>qg&BG@SPpOI%APE8-&~PE4W+hl6!j(lig`#t;3}v$q3DTCr z3nhgi2J8R@C_d)SilU8W^aSt7Bm;dJ81uSXFc9X5!Au~8tBpgUK-=JgK;XGU#obuO z=m&Y5Ov5MDT8*%f7)Wp!pHPVtNkyYcLafil$4E)J++X37qJZ9XduK*}kqUE9kA@4& zf(PZq9gYVb6)wC+kaTJ6K zUx7eF2*417AL^`y{2S(C-kA0i@skM{Mvt32%BTID0<3m7mKWehonM}=Kvo4kV(>%* zI5cL>eZ1T%@8keoa3v7cR$$=Jos@%ctG5be%nDZ|f@L(^zDk_`Dm$3}>48z$}sf@!Y{e838J<96_>r>9}zK&U~ydhJSW zQ{cK#5P(3chIgAEAk$wbHUnphOrIKGI7z!Xmf(;8cw&4~gC%hy#(So7Nf)!*VPVl!MUXXFcAy9%&Yj^Xw8NuO z?Pcl2mBE*v9esvU)45xzJW3wn3D}hn_Vjh3bm|5HYCOlL;ENi=(uxjHigOf!2NwYk z9W2EN6{5uiVSGe!XSVSsC4d1OZ!9B*LElh=zdFO$X0q~2+Wn(w=Y{S9i6B#1 z95gTk1gnUZWi^FLnF>>_jFi#FBJ>t>f5U3uYXM|w%|8WiZ2QwIt%=t1M}(g7TQ))^ z?#9YbM#v6mp&^@J_YBX*r}a}0DY6iO|2AZww?u4SIP$1FfEcEq;J0Tk`wRCn{G^d^%kEK^R0 zp?u^W8zCF)p~Ww-J#kj$?WnvCEJ~A99cGchEYg^QzqF~y4HD!6h?$ zTFqYOPQMUGD<3{B=yq`vWZ+bM!tLUwgX8h@Jm4I0K*8$2cmO}xzIcQA z#_S)lkhttoKBwUD>w2{-`Cto{yx%I$M{!;;Z`E82P)-t8DbX2o1EIj6xDeBipzyiW z;WwUH(aqsZ-7TS23$w*RV3k|rvA$&Zzo26GOc|OzV~(*Y;RAxzqJ)5850;FPFfJ^# zq?E*~)Q6t(1!P+WHAzN9DT`!v)@j%pV4Yk_48FL4I|^4kHl-II5+Vd88)`~HgyoQH z(}aJpiEf2-oh0Y69R?$eCrgT%Iyz%PdK6wRqe4ogE}>;2k`deSPzKAwz!(!}b(+z< z5+-1R<--&Z6}{L&YwRBiJn&JXqk<4nQk5kGX|1H-e#muG9V%*J#NueqTewOAEFLaG zC~4l+82^q6yM^SS9%-fp=%L(}*n}3+!30#oxbWGKC68>Qtx>vZH%)Up^MV#>!=1%A zx8MP;D_~X12EVKFU^y$`F^F9$7C_t8$cikE8~dHTYE ziwGgpL46#PD)w2E;pP;CYVvZJt4bquJE);5f;MhThy&8JZNg9!Y72@64{3L-lnGL> z-=#RW98AF-B}80p5}6a%CZ9H30y zqUnvZQL^FhU*w2Zc z!+XIF#s-$Fy^;F4_XA#dNu0HNmxYzoZBRn-V=NSm0W%plKr#o0zKyB7RCv_(#Lnl`;(7+BfJ1T2WWIX=XKXh>ERoSVsc-0ooI1i#gD$i1; z-L#}-CPt~F26f*lIZ}A^NO^|Pli{cyW7Yzl59vFIt4Cyd4#1Wn+cjW5D6PPHzH7@y zO?o@X@ov~t2vXL?O2pklCQ(zJd7{KPfkuwgPxaKjfMDw0go)bT&aEf)LiI-WqVMSu zqRv3#lfj`^KrAyOYWk;S@JNpa=JmlUL@0js=S{LmP%Gdbe;n>p8P)@r%0L^CHBcZI zRQd<7d+@S&%=Jy~oXTOS(7e^hG%a9Y(mXm85S@kRWTe4{R3Y6i8z~q1lcr{DF}hPT z=gf-B4i{m;tJr15ssIKB2dkWAxiykh5yuE3%t>Md2fhdiau@ZuUv`0ejEOvChZ}dc z2+LZ!&RTlALhPtZVFl};-7R7g0A&qYk5s`QsDe1IZnBuD7#wBT8>Ltp6Qs3lSnVVl z4hmAIk(V7=Ls6liVM9K`26;TNQ{utUOeo%9;g9lkuH2cT+PMrIOfdB&XPWFJFp4`J z%6OrAmhn_M!WS4aaACUqhSfWP(iht&X1Vw&=dh+;!s+BGhft*)f{{7K58t#+>;uk> zbju933ANm|I6Bhc(?f#nP6@EM9IoIho4?du{S>VzF!~s&C@}Q%Wbgy)6lmr~&yj!{ zR(l`-04%SJfGo;EV(*1gjl0_@O;hybqu^7DB}^GR01vu~j{h3~00000Q7w%4&Fn~u zfCDN4lxU&F2|2V9ij0ZMy+F!t=5t~ITN7`^fS56`2NKCWHATP-o%+AW8 z=9)EsY*-i65u+{&sL#*mP(_(^{HMqoP%N6skO;3s9(qaMdr6#q9C-%m`p_=N(3f_A zJTcz`+m=pe~s`i5yV)f9J6aau#0Eh^d(H7Ivuy#Mvg(w@tZ2 z3@0Kj%!x|2I|dglxUR?U3INTYo1Kjy0Rti|tIDgOQaFMPM18s*(u%Z#d+{mexZy{X z(Kyljwl&n6O;o>!O{P$_NP`xgsB|_C&V*eG*^o(qw2IOl-i+X+ zjadNaxV?c-a|D+S(VP&8Tx%ZR+M;De@x`@4X@FxMZV#b>^8V)D$E8FT*T>S{jwUXzzzl#P8=}M%M$um11E^<;oFD!V?xi9?J|csy4{5jLzwdR(pP84#5L2!u=H9dwXALh6lSl?&vcj zwrMsSDnJ16K~O=~*(v4d`BrlZ?YhfUhA;Jro0+efFww^y3!(3<-!6y@K#JVamG zp2#Jjp1ZY(eSH8{{cr-Qk4FU1u3tYw9;k+ps3d9zq?!e$j~vY8@YCX~(@s;Rzgh&(+Sa-WwDoMm>WH zQ3YcxU#}a9bYJCsy8;q&3MNiFhsc^4HL_6s{@E&MB>iJ&3uf$4K$j4zc2;b*R2oF%}TN~Sl&wm70>lb8>w@+Nrg_g%qx1n(Y=H(hH2+ zMN$O{zu|y~WzK0M z>RVadP^j#&$73h+r9wIps1V#B>{c^cwIyaIOd0=u&02!>M)oLx$8S_`0!dN6{3ofK z{jowgLOck#7g?Pu%Idcas5PKLE6$KcuHV|ajt$2s>F6VpL=jX zLsitFPW}`gv^dx9&6mK8@K9OIc{lEZ?ch$C`Xv2VlCY?u-CP;Sg5ozS&74Q@DB&zO zrFD!I!nD|vPuFXZEwY9Cb6g~=jhHuh0;isR1a^R)_WpncEg>itJYedE3|$M^9r2^c zkocwk8lvEWoTL;mQ*sR70|`=cnq59k{7k%lFh_$1=#w#iYVCiB94d>nL}V-O*=_Aj zfT6~|>~9GlM(-nI@L{mLjS0y1_3ZQPx;hb}z$gfasdqw%lC0cpz?r2mp9bdV25lyn z08WyjKUkl2#o0>+5HbgWq_f8?edSfs1$+4=TyO20Pnogb?G`wF;Gv$~c^1IdypbyL zJW_-;Nl?kURU<^A2kjmk$@v)ug@AZvO;aI>Ko99o4WTR*-70XXj&2?81TQw{ey(t% zX6AQjesti*FAA(2sYb$x0W+|gA4t_m-QwP4;Fg+*kEIXXh=%YEO%ADsn0QLPr7zuM zffE_Kfzr!Ill=s-)j}xc(ebep{^N9EVj6I(LTy#|Vm#n`w0R6fY&}h)1N;Y<`Gty} zrlJHUfW*#F?DdB{M7{`QwSXCaV)pj&kU&UTWC^OEwQwgOJ_Ag2zvY%Bm7+VQlsHD3 z6k6R5Err5e*Mlq!2r{HHP3v!phHG6!N2^iDjOW3o|HVzWKc zb@OGpli9873oxOcl!vmG4D1Ua-yMk&AyE*{2lx;%P5>5g1-z_}1l%VJ7!~8ak)tfb zey+vg2=Pds_cfehFv?_dkr4jT{ldIj9>xZ;i#Bi#!!V!*AjSXy5@}OfLXZ8JqA1*u zfcT*SEgUw5tH0jv(5(+}dW{E{s3HU4YHI*hyN$F(n6?Jm9zqpC0y!`I{2KxcGH2BOC)o*^q{T2>fOuhQd4354f^qaa9nz zp7D02#;jc37JEi<;jt_Wu;f?e6nF%p+8>ZP(K>pR0D=tiG+-GJ2`qf%4$@f8KTUvr z&Rw4i7QN?lm!?1-Jm?Mb0>Fr|;uk8>RPwYz%c;;3{}HKDeKA)GN4kguU8<5ddaZ}0 zSWIk5AXO;J^yQF6Z;JPHHr?(g;KU0BMo7t0XQT|jiQqiRWnYJYaRMGHFhq5Q(a^2! zw%i!0q2Of^qQ*Q7^9qV`Gw^2^hK%5pDTGWxH!>Rv8o&M!FDk|GyjC>+EOkNVDno0VZhZ^dQtTx-25OD%-cqfkTPQhNthIG|sFXBWx z@+&4Q=TuS*Ohn=8P#L<#KFCDw(w36QtUqYYp4ZbiDPUva$s0JQNiTlY>H%Bs* ztr1u%sS#EiOAC@JBsXoih5Z8)Cd2bhd5Lq*P_Q%Z7m<`(%AL8OI)y?ZWOq<=86a`E zUS&oUiJEZFvT385L>V75)+Jv+*k35i48&4I_zUTC;S4C@yp1%ji+*}AV{DRC z$yJNt`?AQ=HceuL*50{x4b=6x6*#v5Bdrn`v?jRHZqKnotW89~VBWmf6W^~20Xh+Z|uFnax zE#9*Nn`wkxz{T5Q|0HcQ3}4gP zBNhv8Ya*-u>^&U)Yje9QkHl%|RfWxCYXp)8(qVIj?cT%;XZkM@d(;Lfromu#5k?7` z$o~=B%j`V+FMKf*Z3U}H%u9r8aXj>m1Yfqg%^_YFx1jXIDhgT=QJq$7dF`4(ClU7p zIpI;8ZN)rd&a{~c!=zl=lU0nu8ebMJg$fWJ>zvlwGt(dXjVs0EMg`j8oEveuln5cRsH)_d2-vAO@);i5V23w(;*Od z=SGDEOSQD1w)#c$2A;0~pd3<5qtnM5%Lg6@wTY48= z^pJ=FTY!nMJ}Z(C2(Eu&Re)_FgKWH6v7q<_1|~eIDPapU@x(Io2&@4(z|q77CA_{v zXsoKEzAh6)e^ksEXtL^DTl^V30?N0KVDE@!S48e*;{uQ@eosrdsdYT%4?|-!;qKf( zU*54d*T;pg%9A?jTtSjQT(-ELHLOk*Y`&Pc0&Y46`e^#08Ho2x^k95qODD)p$f{JR z2HI0_Kz!Dj{g-ZftN?O5Rl{eDIH`$1S;aJ{X?|QqBkyFcAs{7fnu&yv>$nqu%-gox zK|pm;7a}0Y9u7M?@s~lQ6tFno>mbro`in0-`P3bx`<+c-9XNk5k}^G5nU#`Jo{IMX zx5zLu{4rX@NH;yxU5KN52C6%)F$JuUP|(7l){1AO{LMiw?P-?BubJRBelBQT62y{i zF-^f2Ye=&7e7HO!aaT1F>RU>FmRKl2=b%92ILDlaHTmSWpHIKKF5e+cA+?yGo2Qp1 zU=y5yX~Xme768~@=0~A0C45YFUm+{tHndmVJ+cxI(_usnkp@vJSBtu^oddVf7R5c| zL`TKgpd=k4{zcBOJEJ5sF(JgIYKwWaR<@MOh>{42fM$Yfag@wD0&55WBYI}&ikV5< z5Ss(V{L#D`x1kF1bbOh4K}RPkxC+vmy`3ZL<1}XVPQ`K}0gUygs0CRB!CPo);A7Ai zxkafSvDw3NwpM>A3kttb4;ob`@oi}+Gt4ZcvJxe_=AVX4IWpSBu#6juqVO_t!J0Vo z>WGh;zkJ$C&bZx=IsXJYuKGeH)7`ANGn;6!z?s-*g;?f<%3MNh-UODeRZTg;d$>(T zwFAqw9+HWeo_#Y;$bat^Nk-pZ3x3DZVY!E*f|p)4YlFKaw zMib}?uBoTe5sEW)4J58ITBSihv4kcHh3Qb_tIv4e1tK2kD~(KPh3DlW>Kb-PQ(_MR za#j8;C$p$f&aC7oS(5Rh&c<)A1LNwJ8?tb>w(1UN5s= z{%EQ$&#Lii^9?<&Y_7t4lH05-T?s`7ULz^FyrEzaf%*Zk3^*D!0yf1JvOL(?s=a89 z+?Anf0rFXu((pV@kl+1H+ELb#w8^+cnF&Oww+{$FZH%H^B1%RS4jS)C@FXi1M=1hr z8RQyen?k@xU7{^JAhea=B6_>A;EMVvb&OWrEK})|;X@W3i|^iu-00iC2s+}4ndd2W z!$+T(f)@xdq!J{YmzXL97YGanVhx1~kwWW1QYJ$I?ABR(UUdzc2(Kr>w;mjuOY<#P zca3lC)8n#y+wL!xrGg}cn5JA#lvPkGl$>w25 zIv7O32}^_`G%%fiXwRZ%Vuak*mM?Xi-Psl?A}A|PKYo2|WS1tgEAo2M(?M0GY9p}` z4{Uc`yQtCO!=;)`G}XrzcOmeHVF*EzErR;XCi>+Aq7*ALLm?`w~= zDMZwgAnybKEj=#!Lwzlv$-`fp&Jf^3AJ9ZVP*8rbe*Gm&OZ%etHzKMM@U_g1w14lrY^Akv*K$5NH$h zv5l0e$NZEZ-gLO3+?2c8!8DdMnpwb3dF)0#wd><4&1BedZ%AgIMXOKDq zsl}s2b)f&LvJTkfpq`(>SWF^x{E?dZvvKa z$b?nN%tLU-1Y`cuaRCP;Fr$}+S)T`$9J4vd;87H*etn>$-1S7vmx?|KAS%l!AP7yA zmg>TXU=EN7N=oBFXu=)=Ajs&vcoPiGT|z{zlEB1*0u!0F5o?C0loRUh=D2cYXo|6k z{sd*QTE-;Ek5z6`!VGl;&wxhM!32e_zNSL8HCY#5t4lv_&8tDoLIT6|fnb2xu|JDu z*`OY{Hes1i1lfOv3gBqu55^^e41*oG25~d0j&!QYJHvR2*!3-fApm2<%dRo}#ZG_} zUp@5>3pNw}tdiGO$*=G^@)#p)QeMSPoKH5JN3nq*F zAo)zxu6A+VepJOBnG0AH7t`3gn1<*I!mo#;)&Y3vHDJdUJ1+eL9Hy6kg(FqQ?-`H~af z7*Sz-5#>B>hRCcOo_2&8KqY`TBPh}`i`@~d^umw$B(Y(y+pYe^#l!O~pI2(Q zxObM;^xfvx>{)JhxXn1c6VttC>wVvB2{E|5f`|5E8}}x!a0M5yM7m2mlxdi@ToTR_ zJnMf;0#_w|^sp_k^1{5rdO+CmR}5UEYuo*CIBzwr0ba!(*c`>SZi9mXQ?_0v3aXbX zC3<8=nIE~lO9MuIeZf1dQcH;~cNPITA0?Q~i5DZBU?r7=SK>g01hV7z+~L2@8{h-L z_q^Z{Ldh6V#*R1j1c1p5SzZWrJZ~hXEtR}S>1!U8N5&?X*UyPX#iI_0_1q2)Bk#5cyF@*o4AT9`GgD~9nRr}6fs zfyFq76XXa2`6`8`L_kzxSdc%UxC(c4ZIvRv!IAX=i{!VAS*bQQK23myTMC1EL|-98 z(4?-7Wt^^B&(+$89;+bL|E(vOAf&DaknghHd?6d(G|3CPYv-x zppdt=U*K6Hh3yCu&Ir(;1{9kfWbka8p+}mBIg124{4-iU4WLCFatr`tTu7sw?hz{5 zP1oK`aCS=8#1~`Al;FG@D9B~}vW$d18q8cV`BKslsj)hfJB5#P8+4Edl1_$wFcOiU ziiG_sVLJzEOKRwQ-;xS#0~T9AXnjn;9#B(q?jq5XLR&+5W5E3Ytpu-5LbF?bZVa0!9f7IFVZ9?kAbHc7h}@ zUYxSYb5_HupjZEv=YR!0)Ps?=*kUpO zp8&wrQ%StF7XqEhRX>s|!#J}e=Qy54843VFoOq3QKcd z+5WO6zj;0@JAaw@R9EEdF4PYr!lN%J&5X$~?Ai{H8Hyd(?7N~{)qztp0Dt0Y`YXDN z_&glr%ApWym$0`^y4wMgjyIkpB<6+FutO$qO463YOpLr{tGkd(-b~?gwjXlRD`PDy zO?uEHqnb)|9ARfb9ObK_0@BxQl#>jEKzs{sihc8nq&TFTRX!}pHdMOW2B_m{`^wS> z#4{TM+NS;dgl+Ov$bf} z6c(sx2C@9CKfk%_0i7&nKoW(jfNC@0IV9c@AYGOLB2bM0YGt}-@04(d0 zuDqio()B7>9x;Ua%cR{n88qffDb+KLoOo~;UVu7xYftjW zkqw|TKMos<$nRd^Nf{O!z4REIC7=%FE^2Yxy2@-oyLA$mv3_d#X(0 zD-QzZW)SADA)%ow``(l`-jj_(Rj_FT2Lm%nd_>pR*!&!LTw^vy#T!@gN)=c1|sk_A%5vaYiB5C{7=QzInuP=@=0a96C0zzc{Cm+$w&Fw^a7p zWM(Y|7(_bj-ebFsJ3tRi!Aq5i<1Lr-FiXWDm!uU!@Kqruy|wzMAleh;3ox; z4jof8!ox6yiV~)_W!h%YU@$g}k_>Qt=X8Lw#Y#<22@ktx3`FnKFjbU&QEIcX#Ry#k ztg~qxvbBt{+Uct|HnUSCAsY7tAZy(Bo|TqWrqoUO1NnVfj|bx(4XJV}z%>vNR)Mfb z8mH)uFQ>t*V;F$D2?M6G`1K5FkWGWNpvTsVslW;GqXA+n2%{biDa#CIcLUT$^p71` z-2wK^h}i>^>;#_*5vK%?5?BjGnq!SZn07WL+{(Ib1F8vAcSl7Kw5WtkP03 zxY)gMpBasj(4g*4dn{NUWID)*u(Z1wTS8JQ-MhXIne7%rjX4a8HwT1eCS--oEKXM- zn3`@nFNO~kp25ta3Rzv*d7#Jdq%75rcCq+d#&ee%;tYJ)4=<6J6=enfYRlTxd1}R& zl^T4YiDZm)bK5U6jkL}u1^=Lm!w{~Nc`c0UYc5Vo4(J$ zw+TVd!6HgyCVcm)%k%$kMn)*RJa3Cs;kbym;1F`SNc)5eEJbEyH;q46*qWV1o4m5^ zApoHO6^yu6P(aEB6sFCVZpEo855|A&r6uYW^E$j=n7A7`M}Z)HB(H@=dH)8S4;*L$mRONipo0kB^ii9%-8qwLZhYB;kPh}=WBSz?MchZ{`ckv15fzGg+W3TuOotLCLrX`zmyigMu`G zeQqjm2cMwmBy2T6F_?kg95drSooO3eRr#$Zi7?}bKjiU?MVC}ShNTW^#$phb_-rMA zc0zPrM}J^2*j}IOs=PN{e1)=&i?vkk9>)db#|VS16Jssc{TK+&S;#QJ=s9KVY5TX~Rp0!dz^T0d$!LU3|9m72mc z$-sD%05KOy&MFtpr6wjgWKhfKfRC`A{I&#Qw1P1!7MoI&OyYv10X1!U+!^Fgf18^M zh~z*Lkiv_)OZm%W9;g{~IC$p;EdV^DSOwuO<`-PwoOUq_!6k{r19&aI_9b5eaw79u zC0)792_hy%_dwxmdBhcgO35YH z$KL)&G$ozySzvT@adNF&_cbv06{I z8F52)jmC&!gdG0_f?_-qarDJbkvJ1*xGB`m`55>CY*tG>hDh*r?VfStoaKES10xOA zG`-{j4=S}BZG^q=4laCVwxFCx@Kx2QHcp{HFHEySnhLZPV7-8N$lk@xVv6=UL=V5$ zjwhv6C#ea>Hn$BHN#6%LvMMBa1TMy-6F40s2pFvm!zu*Ty8+}F_xhS$CctVEiICAV z1VTfg#ZNixYW+G(h=s`zIXV->d#K3eEE!Z9XuBMspzI-0!-S=afTAse+)oA*Ik}aUxEISL-DK)-#Y-&^{|PG` zA;;Z92Db+Zgonl&E6Nokui=n6!3}g|3;(exWLXVGH6vLntFBR>%Yxwd?0(}VNf}+z zFTK=+$nMmL+KI*e2008=FXVoAmuEVlXOgxdKx|PuqDFC*qKoPhF{3o|l|T$GgT7Y+X38)B znDlk??Y&)aF6)ja2I=%1A+eFTk(x4d%s2xd1(h`bn0Ex3e4jjMND4p{-kjj82_Cv& zYdm|$styK7!Mb9^>P(jkw9)TVBlJd1*_!2~7HuyhnXDquh_PwSb|3*HHnB3ylFgNa zkZOkT!ew;oRWG9<@VH4rvK3rv0Y9plGUIV~7Fm&$e_a9?3y8M}>)GgTGXYCiD15^G z#0^G7$^spfFdZEzLf}7Jf_Q5IV`WH?^x~t_W<)~ zR>&ag-`oozywY0S%8K>7!1G` zp0O}G06hid4p^s+B_Tn)ll-dV)aC(h>}YSV;Jn`?bim9MyDTB7KWk7!N>YS49OXwA z7?h>hdAV67z*b<85;DXlhCnY+e!-8T);xKYw+S7ca+456JfDK|3jc(5`| zIV^WjEdfxa%NiVbFpYR0mXaW(En`v}4ocwdOZ=yDkZ=Y&Dr_oDeFvog};?r}&kDEIHbGK2yCs;Vfnp7vFB@F@d-uqtw5o7D1%C#n zAc=_Fw?%5HQdO@g40w?6&OuF=73Lv8@S1CkUEI||(KSK0mHS3_3NDh!-XE_qjP86W zemkt1RhRUnLqQNfpilNX7|LX3`M@vCSm3+lctcrim&wcChK~JHhgw|$fT#W!{w{GX zMR5?dIVQAJVY@;DTP!JI$$fZaxyl<7EGsXUtT?uV6dTH$ERh$N;IfV(Y^1kffG)Vb zc5b#R@T>1JBNUB>MA)uY%IpXbWRBntfFcEjCF_PXNirhtC1l4XfSABPLeaoS^!g(z zaeU7~C5aCuvl_5Ms;mEfa7|6HwN!jVsAGiL(of0N@rg7=lC@3}_9d`78n{;}!T7=t z=Zp;52QA0XmSH0^mDb4P1q3lZSF9YoC(4?JVEfG!{kMvvjPE2DY}((qx2`nIzB@!i z(GU&-Y}W$2I3u(EWv_H2SDwH?hUXT1B0oh+u)qk=Hu`cl^GNra@jay1jC|^BGPD7` zgbd)(77?Yexb4v}`r<()5-rclKAjW8`e1QPY+||H>-=sSVA|^(lkN(G5LW7(>6uL8wiVt;nda%o<`%spD3lP zes@4TG1f8D?Pn0_JCEI zNnO(cMwbn%PW%8SkI?q8C2JAcz}#v+B#?v zhK#tJ%;lc&8hO>jL8kVS2x?(s|h>C%`0x{GH02~fs))_rk#Mul~w>^OBaPG@&wpCxqiW% zN(tb;F$G-~2=~oQLMu)0giTLENYV6V4KGbfof*$UQYC7`%!KmYRM@mK3zr~Oveo$>_cqQ?fIyqVxRRqvz zADWdhOXw`psJFz9iWF4@7e0@>Z!b*)NF+fW+p-cWP7n@wOqsF^WR8#x?TQPnR7xl9 zjWe_ACMqF=In3{q1dq}Me{bx9b)S)QBzMQQ{qqiUd(K$5&A!Yj9q)IMpffD5E)Tx? zv27MWHB0Qpv=Q(^EYYdtDvN;=+My(nDj6?s83GNGR0-tN!r0KbL{R_G)=~s#2{JIjl!Y8s?FEiUHskee9s&Pobv-ASoZ=mMDz+F+oq`Z5@__sz9q52WgNgK{4g&$}OW>RwSJ#t%5pKcN z+gfAQO@$OxCiK<3#2l6_$r6f@?KzW{M0h^tdEAhzr3t8rbsddEU?RoA5MZ;jgD@Uon)zux&h z=i5(W*vx6MX9FzY!osLzF&5EI)u=;z`LqV@aGU~Fpf@X--WbM!*{n6R1pYwTtFf>R z)_=1@l-3w-(526|X|A^ajVBP+Hb;z+ue0Sz3Dh3DM;24{Xoe1VG=ogeL&9e;xQ|7F z7f0S~q6&_;6dC^voZisSW-X+2iTMc{DG8PTvCf{5M87NxuM~_S)uX|SE2ulVI+`JB z0EwVwW6C!qvi{+9Du(iZb*Ph(xLZi(`$+G21_SA3aE}?>i8MRqPB4%%Anibp>gaO^ zd(94}(na{`PB*2E>>Lsjy+;oLIpO5w2(?B?r@d!{+=Id{I%yGWu(&DDT)Fi$A?V8- z;oWL~hazDQC@s0p&h2z%p_{~YKGgtNjn@%9Qn9hMXzk7$L<3Q_?!B=k1&pJHPAw^k z12+a>iIlcIBUx&iEJDqOnB2^NnlGGO$@?CM&A!D?_tG%(khH$RE&4O zkc^Yed6gracsgX5**~l-N|ie2Q(X|y1?Br!jhv|2Esltn zQ%e!QvqAe-X+#GHLQK6npe7nKJjPL)97p?Z93oPH zqnocA&KHcJv-Wmr*GZbq#k!>}eP&B6#)v6}4YU!suf$ESbgq_;>lHm^(o3EDt{Lr> zCVM)a?J{sRU}^t_2E?GiX6Up+S|CnLyUKtSld&*&W7E2!x2jpRawTKBp&k{|A_(}1 zfh53{Ofs045R^0y@^so$1U}N7!F_?bwStfvAf}Bvm}cYeW8iXw2qe&Q;dbRz>|^fK zSG3|tPYvBpHKI*}dZsiydV(P^VM0+8i*D0e7+z_ZS&_R_C^WajISp1tRa!`oyp0^C z7eP&gQPWCmIwX#UoE9W@M4kh{yW^^21^i1Eekx@kXy#Z7a7Ab~pkn_<)ljl@xPgU2Wn+jSHTW7q1Pvb7{XYfF`gJ8D=rAsID z&@|#)1VumugE&GgSxV{zDNmwgUpu9IGqQbN75EX<~s!CNGezs5*Ip;;C_|z6sdQ zqCx?LciVLhSw!ZjE&c26QHq)0AAyCYA5*5`5;6v0Ly}mz1eYP1F{PtphEA*9de0+5 zMvf>X>ax!iEx+-_^;&VN^+cd{*%3qC`=c6=+O_y{^6kFk(H4$cTm`XA`!hMi_!M_0 zPf7>6(E}=euSAcP3fe{iw2Y#8q+v)vB0rhhdMLiAuoLFP$$EFh_s!#^y$CErrey$K zC(%-~I1?Mh(@3I9f~gU7hbL!EAea||@G5bjE_518MF)?B2akj-a5pYcXgCXz^#n+M z4!VL=kQMJ7qA&oQbQTu2hbBiEX&Qj&by&Wez~dNQ$lVx&xujx*^~_Zx5xCcDIfcmf zxsa$Gp2#7(i8W45qEwoohTvJD;IJduR0u2K9DlUb@V~2}6D=dWjC;;2| zi%2bCm+ll24hG1&i)3i*3!>f-9F(Q4$BbCbxgP+MknZ+ryWFF zr7ZDj#8^Rj9f*KJ*oqE$P-;B?pyKNVlXKbP`S|U3t;!G^29U*0v~kSza1A*8s#*aj{p^hY&wDasx|1sT()xhz{vxK5hh;r_DaGXRJA?!aM~U3TM9k_F#NI=|WrBh6#VNPnybrFQe> zsN&=mRnw%H8+QX@P0-&Qa*{LTr9Nl0gDFCP6qlSR^WZ`k{>pkM`5KvaDMy%t(C|)q z22tKJ@T;(K=p{pt8VlrkACEni4q)qs`&Ox>D+VR4jd_?{7P4tEU`L4G5Hm&~GUQzw zNzDA!i-W{pFX8Si1Zt}RF+5zAK(L7pS*ZwGq(HT*IJthPSquPhcK`wUF2-~c?-3)J za7ZQs@M5l6mnmV&Y#^o*2xcZoIeK0C36<(?Bi@y&<`KJ85kq*)y`8 z@MJgk-VIQkm#Z{91b8SoQ9bQ=G8~>EdiF2G6CJdChZzmRS*NPTIs4`H{V1Ymau3Grc)`5xG+Go0jZW=1WdW< zA!SRrK$0qYQ6?-aMo%QkRv{p<1N9-aVz&oTA0AN*D%cRbv{7JysR+J{96%XDY@?=LY%PT3 z6Q1!!vmO7A`lbX75-gC|f^zXB>OdYMKppww&i-f$6zC-(JSm~FB2(fppeu?%Q^q7i zB#UnIs4qx^Ww#zUlGp#&$nXKlFHObFk6Ab`d$Hy^!~ zfv#)RMqCW#uT?;5K<7`=q3?skq(DkwWF|r?E?Ub6uQ+pAPbYxb-AXfCtr`oLiw9+D zoqBUbl`5(#CIHK23mH7qUO-sRV*CF1Zzax}$^(5R=$p)!!(vV~6N1 zGC0KVMi3jROHK?zeT65BEhMWl6BzVB_q?9$Ejuy|TP*(VZID9rmx`oNRFn&kG}=uQ z05RN)L#riVRl!`1N6GZthY(MMBCMkb&4|5{YI`LbhUPzrX*4)iTS#N^2GK9W)?-%_ z*%(fW2*No5PY8A%;G^H(N8$6U20{pM@dAk(8bBz$#nH7G5*21kgtt_I4wWqPESQbV z2k?BHlnqpBG{r}Gs8g4}%=hV48C_P54d`q=I#Dx3dd&}OpqW)j`0w# z4@~E7sARsQDYu*N##tRzgKusJxyt7c;^^Yh+Xaj{0;omDaixHmpFmj1oVEs zj1UCpLh6B#U{{Gnp**3!2Pb(YK(;L{0%H<+M;dA*;uV8q4gz@uE|@`X zEuw{_h$c||v}dx&z~q~P-bOQvVS-5{3yKkUNcJ8Nd*TTLM|euoe`h9j-46^P*po8( zZQK+jRQxGVQr4|)bCHG%Yb}D-YPr&DSWhjojO||%aFbgG<957VVhAOJ{+6f95CxuA zxR%OQYHwzVtf1LLfrO)mP(Zz=O%GqvltE+z85V15{U8HNGLN~ZxngEfzKwatF8aMo zKkfB5Ag_g&kdq)&t$zc`fdOb8dE0P9MELa`XZS@jMmbpksA9{mIS6Jmm9ImDHK~b| zkuvq*XsMFr7^i|@^zjX%z!fe}wDH2~_d^1d5FlnFg$DbG3kIkfXKg5gcr%ZdQ2z=f zMm8V!bU6&qJh}1(PK&#(;T1GRso7aT%|d9fi)+hZ3=2?Hv~dUhmd<#9ka+6VWRLax3=fWA(#jmUG&+$kprEoaYqR+m&a7KxuUNeNZ&sxA)d}%H5{5D+TP@_l|ucG;Sq^Nk*Him$UK#O>d?Aux)5e|wLW|h6sG6SSc*2zayMu;#}G!YN)%pTTm?5NXJ2E+6HS%XQ=nYK}f3v)9p zMOLowxeUTd+%m>!ku!@m_fq3Bsq{sGGFph4yCU!u$pyN;?|adZY9eN=mBXY9@Qp0% z^Dq<@VuGskvRxtc@Uj~KVHg8P#@D)?A%}S_Y>wU(9McNZv9TI53Uf^@Vro#lij^Sl zbDoH&wsq8k6_m5sm{TL~o=!To1}PseDQc*Ia3HMQndtI)k>0y8CbDT6z1P)J8MITR zoyA;@Q`8P7|Ff#Zvw($>@InlkY(La$v1M3q!B?D%D znqN>~R!vOVER#TZZaO2SIa(zNvm!DqoV8p{ZmgI_DxNHGcNj$|yY0Y5?EoK-ujCHB z!V_+i(`Ll6=F@PrzGinZ*cv@tVw$7`EPq}36OgUi%NI>TWMqSt0RBsFn0DI4W^qOY z^)NdpaXOIF+D`0oN128VxyTWTQ4F&wTI%3g!M%}uXmUUb?-ymLt91(lm+pt`%xuSf z-SVE^3tP*p2isRTL!03SVESRRg22eH@q3R?i2mX*aX0vSE`@(Yh}qjQ38WB zYJ(7<^eG5?i{Mpy60)Pok)r^RV6PzYli~o%@xo-(YktTb`LHU)niHlOWk#IiZ2T>c^X2-5g5*Y@p1VzDj&MtO}}^I_IFz8Ua>$ zPa4_%ikB%GljwEZMXWC6ORRIpc+(-=m%X@L2IwAw zssKhXr?JyYJQ;3g1Y{Rc`r-hr@By)avF{nMTj%@O=(~2lTQWH9#d#I%A>ehr?Ya=; zW%4S{l6?UxX8ixqt4b5onV9T{Iq0vOB^GM2f$ROMCCa--X($8lnAMS4V5 zZJ%2BmWJ+Eh+bh7#2hRX?kQ&6bZxj(5|9_n_8hy1)MG@tDjxOf&Iw#X-KKs6oQ=^} zrfO;T1kUS_WdnFL$+0FA8z=OO51hc+gLBpq+ijvk28A#%H#m;+D%`*l2h!bZAy)d_ zd9?{4M-{T$jg&CFa+|}Y2#5Uljfo6QSRDS#>4_`EHJijhr1b;TDmNgTSK3}lGX?C(csv1*v$o%c-&uuv?%b67(qLzIg0L)Y_S*=-YC;_2m zVAKE$9lKnaN<&mvaEbnHzA~@j zz@Z&zQFE-$X7`GAg7F@DU<{H0!!%og@%pun<$ez>@cs$OlxUEc9q`Ah4inTaiHCH! z?I#`FmjN_Doa;kn%&9ua&+LKWg!U<6pv`gh2*Qh{^u3^Y0Op*(Cy7o>9`^MS{)GmpRw+N zI7q_>vK^3u`@sr+H4RkKKQd}I*5s*|X@F)6@CH*Z`5(!a$M?WPr?ty^eQY_LixvB< z@i;W)pz(u*!{i*m%>(`K zmeB%Q={=~-XP**=%fpS-6#h%<%sOnjAW||Rlbx?=2?DSxLq|$ViFH3p%-sx|w^|-C z4Gb`a`ZgHLsg0_$MxlEW9u#^mvwH9}ZJ`J(+c8l=!2)5ou|SbT`BhJpeO=B$O3}c= zFzGEbiSL9Rh3Ia@kTJcLcJ>Z1gsd6P#!xaAh&)#xP>^UIY!Z2oU4<)(lCTK-fw~zJ z;7Ef>$jcDf0wxJ!Gtke2C<%h_o0X#yHc5D*L#kI#CE&@8O5zf^N88Z!yT(sPq!0JW#MXO1PGX*Bq13_555e`k#1qs~{{316&NQiz7J)xS}E+c)Rm~|Lbcz=B1p~ zi(KSa;~J0xR5rhI@M8db9YF&44w4|M&i<} zMSx}U&p1a~C80EdlHGw2&<=n!MJ0HKeP)@EEFgB}Xh_rH7RWcv*{E9(F@mgoxuvhV+CW z08H{7Hs6ge*vu1AlU5|MOn^e1e9+pT zB5Rku(D&n5zetGPF&n zrh9CZO6i<-e09J7iNYe{eUN@vGR#5PmNgjLl0-`qB;M(wZ=iuS@!k-?qJV*jspO)2 z_@Z}#Uv9Ja);jj5I~ZE(*M#N;QIjeXZnkLqNT)YA%0l4VtR!g*m?Wl2dV+>mfszL% zfF88TMi7(jFYZdx5;?UYoAFey2}KT*K~ZW)&4_z7u*V-y^U7G)h^ zLa))n3Z)vb7)&q+2-x<`V7``26RI>LW2?0aANb}6Bc*PTC41p?PDK%+x_~iKd8*hb z&+3QbDjwL^jyo*&iD%20IsT~3As@%=D1bdVf*JIZIDGD6=mi2!{yu%A_cY{+aQuM- zMHC1DHibUOaV3yD%q8{K{|G_M8NvvY;4$o0pGObd5u}BDjU7!DhFIPi_=JWDiwgSO zJHWWHk(cPrL3GlMrt(hVTghuhwnPl3B8M!H)4qLVfV~y_M)IhefLIL3(0B`^QCQJI zMyv4Zx^vGYIT#+REnN$$vyB8BI0a~c-7-l0emw{mO=WE_=l3?EDL%*6ByZtv!s4QO zuOZ}#M3S^r$tSTRZjb}HTIeUuiRLv2=h1N9gyH9!WIkMw!Zo%>h@dAIR?P_ z`0yE!3M~k+Y-FnL(KY&tsrWX7W&DfMqB6{;AgEE@Yg0@0pJaI+o(lm#gFuQ%`V8P= z9j=M_8R(fdJSY@EgvvD*(Am4t z%|X7*Yv}_DC{rLAI)oapRs^H0?&h9irMNz{YN0YA2OzRJcuL9RA=ul} zikOhZV80mwkFoFC;k+iz3{V}Oy?F7qE9o9ZTxn8I#!HMmY_f51eU5_h86KfaP%0tF zI@Sw?=rj*)i6K+`+>L5L@jXdDo8(1Dn-z1Vr6&-DfERMJN6hHrE#Vbt?ogl3!0f#O zP835&B0Jp3UHJze!WI&7H^!y10nFQx{=50Bi!3I`b>CP{YC^+{QBwJ-1TAi^yga6l zm%=1XK6nt644{&!3i?3zx(lx4XM2dw+=Bk5z)`6qMX2%C-xl0SS*s(95tNju_{;M( zqr3Mwi0w$rq^y%At)OQgskBHM*{eORd8XnpsN%U8K(gj`xj?*&V!jVC^UUm?+R+$NkV)(5hKVkA_ zG3M4Z9DMrp(NrqRK8hglK?^XoJI%6St^fcDKy0RhO~%+J49bKelp#$KqM({0u^5SB zMSj@6L}f?s1n*qJTaY^F?mcm8RKT7RKOi6k0;H!!FODH#9R9BmOg!d5i)2LrJ2O^ve(f1?Ce)ULZz6 zQ#T$5h;S_o`?q%utJ(h9Sg>Lhc6*g2Tf;k!ksw*;5-kQ<7e**T>o_o(SjcpmXz{S% z7GjB9dPp(j-ca4dfuT07%HP8*41>;ixxg$H?vDmf76@n==nn zG)tk4MxC371SOYZHif!loyGdVEegmK`xwVKn zez{V~3`VYL#7JxMBfPIe0OV-c3wUYt%nDGbK4L~T!3efs zGroQd`qvowatF1x#hmd&50*^tHRkwZd6|A2-Yzf3zu|OiO;c{Mq_U*N!Mi1 z@Zz&!fFoF`oCKDnLJpjBc{$d!rm=OWP(dN2!z~2RxL5=T8}9c@>urSHZh^YBi(RWT zwP@ar{kZObGcf^|{D!linW&Cg_sWE5bx70s!$P_MK4Uw=liorG z0i7kP0+v`<397==qb~}V$*@Y);p`pvJYy9$Tv-kcRuWJ^QXM|20cR{5K7f+!h+0`3J?nk;b=U}lhbQT-u{^YexDclK3s;em12dkrRe(Yn&odAF6nKS`q5}q;-beHXOI%enx zH+fD<1CJaDVi|(5$nA+s3Jye~*#ShT&w%STQ<1E|YoFN}`Z}vRHch!69#b}grXZxX zP*^55FcM@>gqK)I4Jxt7B&d)I>lL$9NZc5RS`2mdy29G&mz)rVB*fdM=O?QWgB6jw zD})@f8LcRDM;uT?#azwwlmi&eYPw4f2YP@$uA$W24AVR%Ay3@J3|pE+CG*2)hO{h;gF%4V#QN;BWGZ%ZQx ztcwJmLJ7E+ZHs>22S#EBVx=~7mg-i`q$tVq?%=nhWlGdIq=0_GHSyONxO4^m{ZJT3@n_2i?Ec`EJI6RBPwd7$a0w6LbN56X58Zqg@ zDR)c#f|RB?>!BhK|gW%_Y0+K)LRJEw0Msvs57#s=FdIuQ2xgY|^S<|HGlBva6_Kr_0AT@P}1y<`S&SQaT@vv%x> zJu=csC8S+Qu8v$}QyW%H0Zs^(AJ#M6mPxQEfDWB>w{Bd<>Sq3fA|}v$XE?`b8v2u)mfOGQlw`$ z#of&y1|yw8GQzD{9$fv}!=aC^2-hrc041cdOrhg9&bkq?LLcpvD4G@4opC?l- zL#YN(U)KQrqs&}qA#u|pq(j0wKCtmI`h$jj3<}U%ai9vr=tz&DC{e)Fxv8XG36dni zYJ&xqli-_cSU_3gO#Ue62+h$<%onvk+QqXcj<8a;zOp(YoW@1kKH)HSTEo?kQxh?< z>voq21H8YD*3nN1k1}QdN6yl>Y!qf;tVLpOMK#b9w zoy%MYXw|VaPuR-$(D`0Rz#~A7%z#sL67`&Yr#YPAbnT=AsV4MNUo=?wCkgkADW=GEEs})Xox6*WR_T_#-Y!FxageQ zT0PFzBNU9aNC*p-0Iqf$JKl^Lc$#CDwcNa(uVGE;YrDI?Y6{ci4FpYZI-y(ia*`s- z5|JBKbEkfF-nF78$A>`kz?2(_{kv1Py=wu!AJD5mB-sWeOD#-+*lyxKGvwczkLy99Hks*kpJ_DcyAO6_fGL{Hf ztVmQ!Tp7GomhITioA6d!v{$u1uT-E>sBd*tmztxi`_rXzLBUKJmw_M#dGm!>iV__J zzKC#niWHjv0e1o)t6*IVlZy_xF>BEEF6?Pu1hZ(b{A;I)F{y%V&&Nhf5mw-q%0}Yv zOK?AE_Qb8o&R&wGuXEtd_1y1V;}>?64?p9KPgQztr-;BMwA1-Z5~oUOqG`sLAE?|tuA^n3 zOAfRUjd~(vLs8#a31l;aX#E`F?;jlmc64ZVl6P${f~DxDMy%8MfKBEiVRS$$FMM2Z zb4;|~+GVD+s#9rHpBx1-LW5JFEp*$4FDPn_ybhIgf}jG04S+7LTI__=2JJg!4x=1w zs}fUV0dhYf;j*q_~Yru3s=>FJ`Ge)u#VA3-EUG4I0 z==Rv*L}Fu)>$hMsd&bxi6)OR3$^ywTB?W3FLBs0Qw$eBjnrgTB1eIJx8~eqo2`Zoe zC^F+;B<5V)=AJ$aV``=pW#iHx(5Fizp|z}Jp`5SISCf)rY=;#;L^^rn2}P1ZWmcef zp#6S@RFU6*=2)x1-<4KX*4CHTE-;ap!V@T)R9}hN#?qi9oW79Q$w#GKd#13N&Zcl$ zDKPi@4iuSF`$4KC7b6PWCDoR->j(EbrXD>QMJ zhB5XF4oo=i5lH412u(6Ti@)czI8HmDi;-c;-nCa_9Y25n!Ig(g)aDmImaa=x7~q+Z rhmB<>2}j|R2=;6fv*Ztz6$g7z0F3wv71UvW1r>skf=^zPpI1ryYMn@i diff --git a/app/assets/fonts/FontAwesome/fontawesome-webfont.svg b/app/assets/fonts/FontAwesome/fontawesome-webfont.svg deleted file mode 100755 index a9f84695..00000000 --- a/app/assets/fonts/FontAwesome/fontawesome-webfont.svg +++ /dev/null @@ -1,504 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/FontAwesome/fontawesome-webfont.ttf b/app/assets/fonts/FontAwesome/fontawesome-webfont.ttf deleted file mode 100755 index 5cd6cff6d6f6cf438a882e366420dbcc5dddd3f1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 141564 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ƓCICn66&u!PHl6pn& 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 Tl>3eVly1+hIFs?xoxGo5UOcG$ diff --git a/app/assets/fonts/Open_Sans/opensans-bold-webfont.svg b/app/assets/fonts/Open_Sans/opensans-bold-webfont.svg deleted file mode 100644 index 063afd04..00000000 --- a/app/assets/fonts/Open_Sans/opensans-bold-webfont.svg +++ /dev/null @@ -1,1825 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Open_Sans/opensans-bold-webfont.ttf b/app/assets/fonts/Open_Sans/opensans-bold-webfont.ttf deleted file mode 100644 index 711174458cb878281a1a1afe8c2d6ff2351cb080..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 46528 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 Y-+ - - -Generated by Fontastic.me - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/assets/fonts/Streamline/streamline-30px.ttf b/app/assets/fonts/Streamline/streamline-30px.ttf deleted file mode 100644 index c5375dedb20d696036af9378d764dbb594c9c566..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 535160 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

Q|^BJweB$fhkIO{v+*vCI@!Q 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

x`&YqrkCS#F5@3yS&QPz63 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 jTL{SK(4K~WWseKL-+ li { - position: relative; -} -.fa-li { - position: absolute; - left: -2.14285714em; - width: 2.14285714em; - top: 0.14285714em; - text-align: center; -} -.fa-li.fa-lg { - left: -1.85714286em; -} -.fa-border { - padding: 0.2em 0.25em 0.15em; - border: solid 0.08em #eeeeee; - border-radius: 0.1em; -} -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: 0.3em; -} -.fa.pull-right { - margin-left: 0.3em; -} -.fa-spin { - -webkit-animation: spin 2s infinite linear; - -moz-animation: spin 2s infinite linear; - -o-animation: spin 2s infinite linear; - animation: spin 2s infinite linear; -} -@-moz-keyframes spin { - 0% { - -moz-transform: rotate(0deg); - } - 100% { - -moz-transform: rotate(359deg); - } -} -@-webkit-keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - } -} -@-o-keyframes spin { - 0% { - -o-transform: rotate(0deg); - } - 100% { - -o-transform: rotate(359deg); - } -} -@keyframes spin { - 0% { - -webkit-transform: rotate(0deg); - transform: rotate(0deg); - } - 100% { - -webkit-transform: rotate(359deg); - transform: rotate(359deg); - } -} -.fa-rotate-90 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); - -webkit-transform: rotate(90deg); - -moz-transform: rotate(90deg); - -ms-transform: rotate(90deg); - -o-transform: rotate(90deg); - transform: rotate(90deg); -} -.fa-rotate-180 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); - -webkit-transform: rotate(180deg); - -moz-transform: rotate(180deg); - -ms-transform: rotate(180deg); - -o-transform: rotate(180deg); - transform: rotate(180deg); -} -.fa-rotate-270 { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); - -webkit-transform: rotate(270deg); - -moz-transform: rotate(270deg); - -ms-transform: rotate(270deg); - -o-transform: rotate(270deg); - transform: rotate(270deg); -} -.fa-flip-horizontal { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); - -webkit-transform: scale(-1, 1); - -moz-transform: scale(-1, 1); - -ms-transform: scale(-1, 1); - -o-transform: scale(-1, 1); - transform: scale(-1, 1); -} -.fa-flip-vertical { - filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); - -webkit-transform: scale(1, -1); - -moz-transform: scale(1, -1); - -ms-transform: scale(1, -1); - -o-transform: scale(1, -1); - transform: scale(1, -1); -} -.fa-stack { - position: relative; - display: inline-block; - width: 2em; - height: 2em; - line-height: 2em; - vertical-align: middle; -} -.fa-stack-1x, -.fa-stack-2x { - position: absolute; - left: 0; - width: 100%; - text-align: center; -} -.fa-stack-1x { - line-height: inherit; -} -.fa-stack-2x { - font-size: 2em; -} -.fa-inverse { - color: #ffffff; -} -/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen - readers do not read off random characters that represent icons */ -.fa-glass:before { - content: "\f000"; -} -.fa-music:before { - content: "\f001"; -} -.fa-search:before { - content: "\f002"; -} -.fa-envelope-o:before { - content: "\f003"; -} -.fa-heart:before { - content: "\f004"; -} -.fa-star:before { - content: "\f005"; -} -.fa-star-o:before { - content: "\f006"; -} -.fa-user:before { - content: "\f007"; -} -.fa-film:before { - content: "\f008"; -} -.fa-th-large:before { - content: "\f009"; -} -.fa-th:before { - content: "\f00a"; -} -.fa-th-list:before { - content: "\f00b"; -} -.fa-check:before { - content: "\f00c"; -} -.fa-times:before { - content: "\f00d"; -} -.fa-search-plus:before { - content: "\f00e"; -} -.fa-search-minus:before { - content: "\f010"; -} -.fa-power-off:before { - content: "\f011"; -} -.fa-signal:before { - content: "\f012"; -} -.fa-gear:before, -.fa-cog:before { - content: "\f013"; -} -.fa-trash-o:before { - content: "\f014"; -} -.fa-home:before { - content: "\f015"; -} -.fa-file-o:before { - content: "\f016"; -} -.fa-clock-o:before { - content: "\f017"; -} -.fa-road:before { - content: "\f018"; -} -.fa-download:before { - content: "\f019"; -} -.fa-arrow-circle-o-down:before { - content: "\f01a"; -} -.fa-arrow-circle-o-up:before { - content: "\f01b"; -} -.fa-inbox:before { - content: "\f01c"; -} -.fa-play-circle-o:before { - content: "\f01d"; -} -.fa-rotate-right:before, -.fa-repeat:before { - content: "\f01e"; -} -.fa-refresh:before { - content: "\f021"; -} -.fa-list-alt:before { - content: "\f022"; -} -.fa-lock:before { - content: "\f023"; -} -.fa-flag:before { - content: "\f024"; -} -.fa-headphones:before { - content: "\f025"; -} -.fa-volume-off:before { - content: "\f026"; -} -.fa-volume-down:before { - content: "\f027"; -} -.fa-volume-up:before { - content: "\f028"; -} -.fa-qrcode:before { - content: "\f029"; -} -.fa-barcode:before { - content: "\f02a"; -} -.fa-tag:before { - content: "\f02b"; -} -.fa-tags:before { - content: "\f02c"; -} -.fa-book:before { - content: "\f02d"; -} -.fa-bookmark:before { - content: "\f02e"; -} -.fa-print:before { - content: "\f02f"; -} -.fa-camera:before { - content: "\f030"; -} -.fa-font:before { - content: "\f031"; -} -.fa-bold:before { - content: "\f032"; -} -.fa-italic:before { - content: "\f033"; -} -.fa-text-height:before { - content: "\f034"; -} -.fa-text-width:before { - content: "\f035"; -} -.fa-align-left:before { - content: "\f036"; -} -.fa-align-center:before { - content: "\f037"; -} -.fa-align-right:before { - content: "\f038"; -} -.fa-align-justify:before { - content: "\f039"; -} -.fa-list:before { - content: "\f03a"; -} -.fa-dedent:before, -.fa-outdent:before { - content: "\f03b"; -} -.fa-indent:before { - content: "\f03c"; -} -.fa-video-camera:before { - content: "\f03d"; -} -.fa-photo:before, -.fa-image:before, -.fa-picture-o:before { - content: "\f03e"; -} -.fa-pencil:before { - content: "\f040"; -} -.fa-map-marker:before { - content: "\f041"; -} -.fa-adjust:before { - content: "\f042"; -} -.fa-tint:before { - content: "\f043"; -} -.fa-edit:before, -.fa-pencil-square-o:before { - content: "\f044"; -} -.fa-share-square-o:before { - content: "\f045"; -} -.fa-check-square-o:before { - content: "\f046"; -} -.fa-arrows:before { - content: "\f047"; -} -.fa-step-backward:before { - content: "\f048"; -} -.fa-fast-backward:before { - content: "\f049"; -} -.fa-backward:before { - content: "\f04a"; -} -.fa-play:before { - content: "\f04b"; -} -.fa-pause:before { - content: "\f04c"; -} -.fa-stop:before { - content: "\f04d"; -} -.fa-forward:before { - content: "\f04e"; -} -.fa-fast-forward:before { - content: "\f050"; -} -.fa-step-forward:before { - content: "\f051"; -} -.fa-eject:before { - content: "\f052"; -} -.fa-chevron-left:before { - content: "\f053"; -} -.fa-chevron-right:before { - content: "\f054"; -} -.fa-plus-circle:before { - content: "\f055"; -} -.fa-minus-circle:before { - content: "\f056"; -} -.fa-times-circle:before { - content: "\f057"; -} -.fa-check-circle:before { - content: "\f058"; -} -.fa-question-circle:before { - content: "\f059"; -} -.fa-info-circle:before { - content: "\f05a"; -} -.fa-crosshairs:before { - content: "\f05b"; -} -.fa-times-circle-o:before { - content: "\f05c"; -} -.fa-check-circle-o:before { - content: "\f05d"; -} -.fa-ban:before { - content: "\f05e"; -} -.fa-arrow-left:before { - content: "\f060"; -} -.fa-arrow-right:before { - content: "\f061"; -} -.fa-arrow-up:before { - content: "\f062"; -} -.fa-arrow-down:before { - content: "\f063"; -} -.fa-mail-forward:before, -.fa-share:before { - content: "\f064"; -} -.fa-expand:before { - content: "\f065"; -} -.fa-compress:before { - content: "\f066"; -} -.fa-plus:before { - content: "\f067"; -} -.fa-minus:before { - content: "\f068"; -} -.fa-asterisk:before { - content: "\f069"; -} -.fa-exclamation-circle:before { - content: "\f06a"; -} -.fa-gift:before { - content: "\f06b"; -} -.fa-leaf:before { - content: "\f06c"; -} -.fa-fire:before { - content: "\f06d"; -} -.fa-eye:before { - content: "\f06e"; -} -.fa-eye-slash:before { - content: "\f070"; -} -.fa-warning:before, -.fa-exclamation-triangle:before { - content: "\f071"; -} -.fa-plane:before { - content: "\f072"; -} -.fa-calendar:before { - content: "\f073"; -} -.fa-random:before { - content: "\f074"; -} -.fa-comment:before { - content: "\f075"; -} -.fa-magnet:before { - content: "\f076"; -} -.fa-chevron-up:before { - content: "\f077"; -} -.fa-chevron-down:before { - content: "\f078"; -} -.fa-retweet:before { - content: "\f079"; -} -.fa-shopping-cart:before { - content: "\f07a"; -} -.fa-folder:before { - content: "\f07b"; -} -.fa-folder-open:before { - content: "\f07c"; -} -.fa-arrows-v:before { - content: "\f07d"; -} -.fa-arrows-h:before { - content: "\f07e"; -} -.fa-bar-chart-o:before { - content: "\f080"; -} -.fa-twitter-square:before { - content: "\f081"; -} -.fa-facebook-square:before { - content: "\f082"; -} -.fa-camera-retro:before { - content: "\f083"; -} -.fa-key:before { - content: "\f084"; -} -.fa-gears:before, -.fa-cogs:before { - content: "\f085"; -} -.fa-comments:before { - content: "\f086"; -} -.fa-thumbs-o-up:before { - content: "\f087"; -} -.fa-thumbs-o-down:before { - content: "\f088"; -} -.fa-star-half:before { - content: "\f089"; -} -.fa-heart-o:before { - content: "\f08a"; -} -.fa-sign-out:before { - content: "\f08b"; -} -.fa-linkedin-square:before { - content: "\f08c"; -} -.fa-thumb-tack:before { - content: "\f08d"; -} -.fa-external-link:before { - content: "\f08e"; -} -.fa-sign-in:before { - content: "\f090"; -} -.fa-trophy:before { - content: "\f091"; -} -.fa-github-square:before { - content: "\f092"; -} -.fa-upload:before { - content: "\f093"; -} -.fa-lemon-o:before { - content: "\f094"; -} -.fa-phone:before { - content: "\f095"; -} -.fa-square-o:before { - content: "\f096"; -} -.fa-bookmark-o:before { - content: "\f097"; -} -.fa-phone-square:before { - content: "\f098"; -} -.fa-twitter:before { - content: "\f099"; -} -.fa-facebook:before { - content: "\f09a"; -} -.fa-github:before { - content: "\f09b"; -} -.fa-unlock:before { - content: "\f09c"; -} -.fa-credit-card:before { - content: "\f09d"; -} -.fa-rss:before { - content: "\f09e"; -} -.fa-hdd-o:before { - content: "\f0a0"; -} -.fa-bullhorn:before { - content: "\f0a1"; -} -.fa-bell:before { - content: "\f0f3"; -} -.fa-certificate:before { - content: "\f0a3"; -} -.fa-hand-o-right:before { - content: "\f0a4"; -} -.fa-hand-o-left:before { - content: "\f0a5"; -} -.fa-hand-o-up:before { - content: "\f0a6"; -} -.fa-hand-o-down:before { - content: "\f0a7"; -} -.fa-arrow-circle-left:before { - content: "\f0a8"; -} -.fa-arrow-circle-right:before { - content: "\f0a9"; -} -.fa-arrow-circle-up:before { - content: "\f0aa"; -} -.fa-arrow-circle-down:before { - content: "\f0ab"; -} -.fa-globe:before { - content: "\f0ac"; -} -.fa-wrench:before { - content: "\f0ad"; -} -.fa-tasks:before { - content: "\f0ae"; -} -.fa-filter:before { - content: "\f0b0"; -} -.fa-briefcase:before { - content: "\f0b1"; -} -.fa-arrows-alt:before { - content: "\f0b2"; -} -.fa-group:before, -.fa-users:before { - content: "\f0c0"; -} -.fa-chain:before, -.fa-link:before { - content: "\f0c1"; -} -.fa-cloud:before { - content: "\f0c2"; -} -.fa-flask:before { - content: "\f0c3"; -} -.fa-cut:before, -.fa-scissors:before { - content: "\f0c4"; -} -.fa-copy:before, -.fa-files-o:before { - content: "\f0c5"; -} -.fa-paperclip:before { - content: "\f0c6"; -} -.fa-save:before, -.fa-floppy-o:before { - content: "\f0c7"; -} -.fa-square:before { - content: "\f0c8"; -} -.fa-navicon:before, -.fa-reorder:before, -.fa-bars:before { - content: "\f0c9"; -} -.fa-list-ul:before { - content: "\f0ca"; -} -.fa-list-ol:before { - content: "\f0cb"; -} -.fa-strikethrough:before { - content: "\f0cc"; -} -.fa-underline:before { - content: "\f0cd"; -} -.fa-table:before { - content: "\f0ce"; -} -.fa-magic:before { - content: "\f0d0"; -} -.fa-truck:before { - content: "\f0d1"; -} -.fa-pinterest:before { - content: "\f0d2"; -} -.fa-pinterest-square:before { - content: "\f0d3"; -} -.fa-google-plus-square:before { - content: "\f0d4"; -} -.fa-google-plus:before { - content: "\f0d5"; -} -.fa-money:before { - content: "\f0d6"; -} -.fa-caret-down:before { - content: "\f0d7"; -} -.fa-caret-up:before { - content: "\f0d8"; -} -.fa-caret-left:before { - content: "\f0d9"; -} -.fa-caret-right:before { - content: "\f0da"; -} -.fa-columns:before { - content: "\f0db"; -} -.fa-unsorted:before, -.fa-sort:before { - content: "\f0dc"; -} -.fa-sort-down:before, -.fa-sort-desc:before { - content: "\f0dd"; -} -.fa-sort-up:before, -.fa-sort-asc:before { - content: "\f0de"; -} -.fa-envelope:before { - content: "\f0e0"; -} -.fa-linkedin:before { - content: "\f0e1"; -} -.fa-rotate-left:before, -.fa-undo:before { - content: "\f0e2"; -} -.fa-legal:before, -.fa-gavel:before { - content: "\f0e3"; -} -.fa-dashboard:before, -.fa-tachometer:before { - content: "\f0e4"; -} -.fa-comment-o:before { - content: "\f0e5"; -} -.fa-comments-o:before { - content: "\f0e6"; -} -.fa-flash:before, -.fa-bolt:before { - content: "\f0e7"; -} -.fa-sitemap:before { - content: "\f0e8"; -} -.fa-umbrella:before { - content: "\f0e9"; -} -.fa-paste:before, -.fa-clipboard:before { - content: "\f0ea"; -} -.fa-lightbulb-o:before { - content: "\f0eb"; -} -.fa-exchange:before { - content: "\f0ec"; -} -.fa-cloud-download:before { - content: "\f0ed"; -} -.fa-cloud-upload:before { - content: "\f0ee"; -} -.fa-user-md:before { - content: "\f0f0"; -} -.fa-stethoscope:before { - content: "\f0f1"; -} -.fa-suitcase:before { - content: "\f0f2"; -} -.fa-bell-o:before { - content: "\f0a2"; -} -.fa-coffee:before { - content: "\f0f4"; -} -.fa-cutlery:before { - content: "\f0f5"; -} -.fa-file-text-o:before { - content: "\f0f6"; -} -.fa-building-o:before { - content: "\f0f7"; -} -.fa-hospital-o:before { - content: "\f0f8"; -} -.fa-ambulance:before { - content: "\f0f9"; -} -.fa-medkit:before { - content: "\f0fa"; -} -.fa-fighter-jet:before { - content: "\f0fb"; -} -.fa-beer:before { - content: "\f0fc"; -} -.fa-h-square:before { - content: "\f0fd"; -} -.fa-plus-square:before { - content: "\f0fe"; -} -.fa-angle-double-left:before { - content: "\f100"; -} -.fa-angle-double-right:before { - content: "\f101"; -} -.fa-angle-double-up:before { - content: "\f102"; -} -.fa-angle-double-down:before { - content: "\f103"; -} -.fa-angle-left:before { - content: "\f104"; -} -.fa-angle-right:before { - content: "\f105"; -} -.fa-angle-up:before { - content: "\f106"; -} -.fa-angle-down:before { - content: "\f107"; -} -.fa-desktop:before { - content: "\f108"; -} -.fa-laptop:before { - content: "\f109"; -} -.fa-tablet:before { - content: "\f10a"; -} -.fa-mobile-phone:before, -.fa-mobile:before { - content: "\f10b"; -} -.fa-circle-o:before { - content: "\f10c"; -} -.fa-quote-left:before { - content: "\f10d"; -} -.fa-quote-right:before { - content: "\f10e"; -} -.fa-spinner:before { - content: "\f110"; -} -.fa-circle:before { - content: "\f111"; -} -.fa-mail-reply:before, -.fa-reply:before { - content: "\f112"; -} -.fa-github-alt:before { - content: "\f113"; -} -.fa-folder-o:before { - content: "\f114"; -} -.fa-folder-open-o:before { - content: "\f115"; -} -.fa-smile-o:before { - content: "\f118"; -} -.fa-frown-o:before { - content: "\f119"; -} -.fa-meh-o:before { - content: "\f11a"; -} -.fa-gamepad:before { - content: "\f11b"; -} -.fa-keyboard-o:before { - content: "\f11c"; -} -.fa-flag-o:before { - content: "\f11d"; -} -.fa-flag-checkered:before { - content: "\f11e"; -} -.fa-terminal:before { - content: "\f120"; -} -.fa-code:before { - content: "\f121"; -} -.fa-mail-reply-all:before, -.fa-reply-all:before { - content: "\f122"; -} -.fa-star-half-empty:before, -.fa-star-half-full:before, -.fa-star-half-o:before { - content: "\f123"; -} -.fa-location-arrow:before { - content: "\f124"; -} -.fa-crop:before { - content: "\f125"; -} -.fa-code-fork:before { - content: "\f126"; -} -.fa-unlink:before, -.fa-chain-broken:before { - content: "\f127"; -} -.fa-question:before { - content: "\f128"; -} -.fa-info:before { - content: "\f129"; -} -.fa-exclamation:before { - content: "\f12a"; -} -.fa-superscript:before { - content: "\f12b"; -} -.fa-subscript:before { - content: "\f12c"; -} -.fa-eraser:before { - content: "\f12d"; -} -.fa-puzzle-piece:before { - content: "\f12e"; -} -.fa-microphone:before { - content: "\f130"; -} -.fa-microphone-slash:before { - content: "\f131"; -} -.fa-shield:before { - content: "\f132"; -} -.fa-calendar-o:before { - content: "\f133"; -} -.fa-fire-extinguisher:before { - content: "\f134"; -} -.fa-rocket:before { - content: "\f135"; -} -.fa-maxcdn:before { - content: "\f136"; -} -.fa-chevron-circle-left:before { - content: "\f137"; -} -.fa-chevron-circle-right:before { - content: "\f138"; -} -.fa-chevron-circle-up:before { - content: "\f139"; -} -.fa-chevron-circle-down:before { - content: "\f13a"; -} -.fa-html5:before { - content: "\f13b"; -} -.fa-css3:before { - content: "\f13c"; -} -.fa-anchor:before { - content: "\f13d"; -} -.fa-unlock-alt:before { - content: "\f13e"; -} -.fa-bullseye:before { - content: "\f140"; -} -.fa-ellipsis-h:before { - content: "\f141"; -} -.fa-ellipsis-v:before { - content: "\f142"; -} -.fa-rss-square:before { - content: "\f143"; -} -.fa-play-circle:before { - content: "\f144"; -} -.fa-ticket:before { - content: "\f145"; -} -.fa-minus-square:before { - content: "\f146"; -} -.fa-minus-square-o:before { - content: "\f147"; -} -.fa-level-up:before { - content: "\f148"; -} -.fa-level-down:before { - content: "\f149"; -} -.fa-check-square:before { - content: "\f14a"; -} -.fa-pencil-square:before { - content: "\f14b"; -} -.fa-external-link-square:before { - content: "\f14c"; -} -.fa-share-square:before { - content: "\f14d"; -} -.fa-compass:before { - content: "\f14e"; -} -.fa-toggle-down:before, -.fa-caret-square-o-down:before { - content: "\f150"; -} -.fa-toggle-up:before, -.fa-caret-square-o-up:before { - content: "\f151"; -} -.fa-toggle-right:before, -.fa-caret-square-o-right:before { - content: "\f152"; -} -.fa-euro:before, -.fa-eur:before { - content: "\f153"; -} -.fa-gbp:before { - content: "\f154"; -} -.fa-dollar:before, -.fa-usd:before { - content: "\f155"; -} -.fa-rupee:before, -.fa-inr:before { - content: "\f156"; -} -.fa-cny:before, -.fa-rmb:before, -.fa-yen:before, -.fa-jpy:before { - content: "\f157"; -} -.fa-ruble:before, -.fa-rouble:before, -.fa-rub:before { - content: "\f158"; -} -.fa-won:before, -.fa-krw:before { - content: "\f159"; -} -.fa-bitcoin:before, -.fa-btc:before { - content: "\f15a"; -} -.fa-file:before { - content: "\f15b"; -} -.fa-file-text:before { - content: "\f15c"; -} -.fa-sort-alpha-asc:before { - content: "\f15d"; -} -.fa-sort-alpha-desc:before { - content: "\f15e"; -} -.fa-sort-amount-asc:before { - content: "\f160"; -} -.fa-sort-amount-desc:before { - content: "\f161"; -} -.fa-sort-numeric-asc:before { - content: "\f162"; -} -.fa-sort-numeric-desc:before { - content: "\f163"; -} -.fa-thumbs-up:before { - content: "\f164"; -} -.fa-thumbs-down:before { - content: "\f165"; -} -.fa-youtube-square:before { - content: "\f166"; -} -.fa-youtube:before { - content: "\f167"; -} -.fa-xing:before { - content: "\f168"; -} -.fa-xing-square:before { - content: "\f169"; -} -.fa-youtube-play:before { - content: "\f16a"; -} -.fa-dropbox:before { - content: "\f16b"; -} -.fa-stack-overflow:before { - content: "\f16c"; -} -.fa-instagram:before { - content: "\f16d"; -} -.fa-flickr:before { - content: "\f16e"; -} -.fa-adn:before { - content: "\f170"; -} -.fa-bitbucket:before { - content: "\f171"; -} -.fa-bitbucket-square:before { - content: "\f172"; -} -.fa-tumblr:before { - content: "\f173"; -} -.fa-tumblr-square:before { - content: "\f174"; -} -.fa-long-arrow-down:before { - content: "\f175"; -} -.fa-long-arrow-up:before { - content: "\f176"; -} -.fa-long-arrow-left:before { - content: "\f177"; -} -.fa-long-arrow-right:before { - content: "\f178"; -} -.fa-apple:before { - content: "\f179"; -} -.fa-windows:before { - content: "\f17a"; -} -.fa-android:before { - content: "\f17b"; -} -.fa-linux:before { - content: "\f17c"; -} -.fa-dribbble:before { - content: "\f17d"; -} -.fa-skype:before { - content: "\f17e"; -} -.fa-foursquare:before { - content: "\f180"; -} -.fa-trello:before { - content: "\f181"; -} -.fa-female:before { - content: "\f182"; -} -.fa-male:before { - content: "\f183"; -} -.fa-gittip:before { - content: "\f184"; -} -.fa-sun-o:before { - content: "\f185"; -} -.fa-moon-o:before { - content: "\f186"; -} -.fa-archive:before { - content: "\f187"; -} -.fa-bug:before { - content: "\f188"; -} -.fa-vk:before { - content: "\f189"; -} -.fa-weibo:before { - content: "\f18a"; -} -.fa-renren:before { - content: "\f18b"; -} -.fa-pagelines:before { - content: "\f18c"; -} -.fa-stack-exchange:before { - content: "\f18d"; -} -.fa-arrow-circle-o-right:before { - content: "\f18e"; -} -.fa-arrow-circle-o-left:before { - content: "\f190"; -} -.fa-toggle-left:before, -.fa-caret-square-o-left:before { - content: "\f191"; -} -.fa-dot-circle-o:before { - content: "\f192"; -} -.fa-wheelchair:before { - content: "\f193"; -} -.fa-vimeo-square:before { - content: "\f194"; -} -.fa-turkish-lira:before, -.fa-try:before { - content: "\f195"; -} -.fa-plus-square-o:before { - content: "\f196"; -} -.fa-space-shuttle:before { - content: "\f197"; -} -.fa-slack:before { - content: "\f198"; -} -.fa-envelope-square:before { - content: "\f199"; -} -.fa-wordpress:before { - content: "\f19a"; -} -.fa-openid:before { - content: "\f19b"; -} -.fa-institution:before, -.fa-bank:before, -.fa-university:before { - content: "\f19c"; -} -.fa-mortar-board:before, -.fa-graduation-cap:before { - content: "\f19d"; -} -.fa-yahoo:before { - content: "\f19e"; -} -.fa-google:before { - content: "\f1a0"; -} -.fa-reddit:before { - content: "\f1a1"; -} -.fa-reddit-square:before { - content: "\f1a2"; -} -.fa-stumbleupon-circle:before { - content: "\f1a3"; -} -.fa-stumbleupon:before { - content: "\f1a4"; -} -.fa-delicious:before { - content: "\f1a5"; -} -.fa-digg:before { - content: "\f1a6"; -} -.fa-pied-piper-square:before, -.fa-pied-piper:before { - content: "\f1a7"; -} -.fa-pied-piper-alt:before { - content: "\f1a8"; -} -.fa-drupal:before { - content: "\f1a9"; -} -.fa-joomla:before { - content: "\f1aa"; -} -.fa-language:before { - content: "\f1ab"; -} -.fa-fax:before { - content: "\f1ac"; -} -.fa-building:before { - content: "\f1ad"; -} -.fa-child:before { - content: "\f1ae"; -} -.fa-paw:before { - content: "\f1b0"; -} -.fa-spoon:before { - content: "\f1b1"; -} -.fa-cube:before { - content: "\f1b2"; -} -.fa-cubes:before { - content: "\f1b3"; -} -.fa-behance:before { - content: "\f1b4"; -} -.fa-behance-square:before { - content: "\f1b5"; -} -.fa-steam:before { - content: "\f1b6"; -} -.fa-steam-square:before { - content: "\f1b7"; -} -.fa-recycle:before { - content: "\f1b8"; -} -.fa-automobile:before, -.fa-car:before { - content: "\f1b9"; -} -.fa-cab:before, -.fa-taxi:before { - content: "\f1ba"; -} -.fa-tree:before { - content: "\f1bb"; -} -.fa-spotify:before { - content: "\f1bc"; -} -.fa-deviantart:before { - content: "\f1bd"; -} -.fa-soundcloud:before { - content: "\f1be"; -} -.fa-database:before { - content: "\f1c0"; -} -.fa-file-pdf-o:before { - content: "\f1c1"; -} -.fa-file-word-o:before { - content: "\f1c2"; -} -.fa-file-excel-o:before { - content: "\f1c3"; -} -.fa-file-powerpoint-o:before { - content: "\f1c4"; -} -.fa-file-photo-o:before, -.fa-file-picture-o:before, -.fa-file-image-o:before { - content: "\f1c5"; -} -.fa-file-zip-o:before, -.fa-file-archive-o:before { - content: "\f1c6"; -} -.fa-file-sound-o:before, -.fa-file-audio-o:before { - content: "\f1c7"; -} -.fa-file-movie-o:before, -.fa-file-video-o:before { - content: "\f1c8"; -} -.fa-file-code-o:before { - content: "\f1c9"; -} -.fa-vine:before { - content: "\f1ca"; -} -.fa-codepen:before { - content: "\f1cb"; -} -.fa-jsfiddle:before { - content: "\f1cc"; -} -.fa-life-bouy:before, -.fa-life-saver:before, -.fa-support:before, -.fa-life-ring:before { - content: "\f1cd"; -} -.fa-circle-o-notch:before { - content: "\f1ce"; -} -.fa-ra:before, -.fa-rebel:before { - content: "\f1d0"; -} -.fa-ge:before, -.fa-empire:before { - content: "\f1d1"; -} -.fa-git-square:before { - content: "\f1d2"; -} -.fa-git:before { - content: "\f1d3"; -} -.fa-hacker-news:before { - content: "\f1d4"; -} -.fa-tencent-weibo:before { - content: "\f1d5"; -} -.fa-qq:before { - content: "\f1d6"; -} -.fa-wechat:before, -.fa-weixin:before { - content: "\f1d7"; -} -.fa-send:before, -.fa-paper-plane:before { - content: "\f1d8"; -} -.fa-send-o:before, -.fa-paper-plane-o:before { - content: "\f1d9"; -} -.fa-history:before { - content: "\f1da"; -} -.fa-circle-thin:before { - content: "\f1db"; -} -.fa-header:before { - content: "\f1dc"; -} -.fa-paragraph:before { - content: "\f1dd"; -} -.fa-sliders:before { - content: "\f1de"; -} -.fa-share-alt:before { - content: "\f1e0"; -} -.fa-share-alt-square:before { - content: "\f1e1"; -} -.fa-bomb:before { - content: "\f1e2"; -} diff --git a/app/assets/stylesheets/boot/google-webfonts.css.scss b/app/assets/stylesheets/boot/google-webfonts.css.scss deleted file mode 100644 index e3d6068d..00000000 --- a/app/assets/stylesheets/boot/google-webfonts.css.scss +++ /dev/null @@ -1,79 +0,0 @@ -// License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> - -/* Open Sans */ - -@font-face { - font-family: "Open Sans"; - src: font-url("Open_Sans/opensans-regular-webfont.eot"); - src: font-url("Open_Sans/opensans-regular-webfont.eot?#iefix"), - format("embedded-opentype"), - font-url("Open_Sans/opensans-regular-webfont.woff") format("woff"), - font-url("Open_Sans/opensans-regular-webfont.ttf") format("truetype"), - font-url("Open_Sans/opensans-regular-webfont.svg#open_sansregular") - format("svg"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: "Open Sans"; - src: font-url("Open_Sans/opensans-light-webfont.eot"); - src: font-url("Open_Sans/opensans-light-webfont.eot?#iefix") - format("embedded-opentype"), - font-url("Open_Sans/opensans-light-webfont.woff") format("woff"), - font-url("Open_Sans/opensans-light-webfont.ttf") format("truetype"), - font-url("Open_Sans/opensans-light-webfont.svg#open_sanslight") - format("svg"); - font-weight: 200; - font-style: normal; -} - -@font-face { - font-family: "Open Sans"; - src: font-url("Open_Sans/opensans-bold-webfont.eot"); - src: font-url("Open_Sans/opensans-bold-webfont.eot?#iefix") - format("embedded-opentype"), - font-url("Open_Sans/opensans-bold-webfont.woff") format("woff"), - font-url("Open_Sans/opensans-bold-webfont.ttf") format("truetype"), - font-url("Open_Sans/opensans-bold-webfont.svg#open_sansbold") format("svg"); - font-weight: bold; - font-style: normal; -} - -/* Bitter */ - -@font-face { - font-family: "OpenSansCondensed"; - src: font-url("Open_Sans_Condensed/opensans-condbold-webfont.eot"); - src: font-url("Open_Sans_Condensed/opensans-condbold-webfont.eot?#iefix") - format("embedded-opentype"), - font-url("Open_Sans_Condensed/opensans-condbold-webfont.woff") - format("woff"), - font-url("Open_Sans_Condensed/opensans-condbold-webfont.ttf") - format("truetype"), - font-url("Open_Sans_Condensed/opensans-condbold-webfont.svg") format("svg"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: "Bitter"; - src: font-url("Bitter/Bitter-Regular.eot"); - src: font-url("Bitter/Bitter-Regular.eot?#iefix") format("embedded-opentype"), - font-url("Bitter/Bitter-Regular.woff") format("woff"), - font-url("Bitter/Bitter-Regular.ttf") format("truetype"), - font-url("Bitter/Bitter-Regular.svg#bitterregular") format("svg"); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: "Bitter"; - src: font-url("Bitter/Bitter-Bold.eot"); - src: font-url("Bitter/Bitter-Bold.eot?#iefix") format("embedded-opentype"), - font-url("Bitter/Bitter-Bold.woff") format("woff"), - font-url("Bitter/Bitter-Bold.ttf") format("truetype"), - font-url("Bitter/Bitter-Bold.svg#bitterbold") format("svg"); - font-weight: bold; - font-style: normal; -} diff --git a/app/assets/stylesheets/boot/streamline-icons.css.scss b/app/assets/stylesheets/boot/streamline-icons.css.scss index 128509cb..5bb3150e 100644 --- a/app/assets/stylesheets/boot/streamline-icons.css.scss +++ b/app/assets/stylesheets/boot/streamline-icons.css.scss @@ -3,12 +3,7 @@ @font-face { font-family: "streamline-30px"; - src: font-url("Streamline/streamline-30px.eot"); - src: font-url("Streamline/streamline-30px.eot?#iefix") - format("embedded-opentype"), - font-url("Streamline/streamline-30px.woff") format("woff"), - font-url("Streamline/streamline-30px.ttf") format("truetype"), - font-url("Streamline/streamline-30px.svg#streamline-30px") format("svg"); + src: font-url("Streamline/streamline-30px.woff") format("woff"); font-weight: normal; font-style: normal; } From 45b3c436ae1cba75a98305f2e875f1100ea2f9aa Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 11 Feb 2020 12:55:12 -0600 Subject: [PATCH 271/440] Re-add the style sheets accidentally deleted --- .../stylesheets/boot/font-awesome.css.scss | 1565 +++++++++++++++++ .../stylesheets/boot/google-webfonts.css.scss | 48 + 2 files changed, 1613 insertions(+) create mode 100644 app/assets/stylesheets/boot/font-awesome.css.scss create mode 100644 app/assets/stylesheets/boot/google-webfonts.css.scss diff --git a/app/assets/stylesheets/boot/font-awesome.css.scss b/app/assets/stylesheets/boot/font-awesome.css.scss new file mode 100644 index 00000000..b88ea8e8 --- /dev/null +++ b/app/assets/stylesheets/boot/font-awesome.css.scss @@ -0,0 +1,1565 @@ +// License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +/*! + * Font Awesome 4.1.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */ + +@font-face { + font-family: "FontAwesome"; + src: font-url("FontAwesome/fontawesome-webfont.woff?v=4.1.0") format("woff"); + font-weight: normal; + font-style: normal; +} +.fa { + display: inline-block; + font-family: FontAwesome; + font-style: normal; + font-weight: normal; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} +/* makes the font 33% larger relative to the icon container */ +.fa-lg { + font-size: 1.33333333em; + line-height: 0.75em; + vertical-align: -15%; +} +.fa-2x { + font-size: 2em; +} +.fa-3x { + font-size: 3em; +} +.fa-4x { + font-size: 4em; +} +.fa-5x { + font-size: 5em; +} +.fa-fw { + width: 1.28571429em; + text-align: center; +} +.fa-ul { + padding-left: 0; + margin-left: 2.14285714em; + list-style-type: none; +} +.fa-ul > li { + position: relative; +} +.fa-li { + position: absolute; + left: -2.14285714em; + width: 2.14285714em; + top: 0.14285714em; + text-align: center; +} +.fa-li.fa-lg { + left: -1.85714286em; +} +.fa-border { + padding: 0.2em 0.25em 0.15em; + border: solid 0.08em #eeeeee; + border-radius: 0.1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: 0.3em; +} +.fa.pull-right { + margin-left: 0.3em; +} +.fa-spin { + -webkit-animation: spin 2s infinite linear; + -moz-animation: spin 2s infinite linear; + -o-animation: spin 2s infinite linear; + animation: spin 2s infinite linear; +} +@-moz-keyframes spin { + 0% { + -moz-transform: rotate(0deg); + } + 100% { + -moz-transform: rotate(359deg); + } +} +@-webkit-keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + } +} +@-o-keyframes spin { + 0% { + -o-transform: rotate(0deg); + } + 100% { + -o-transform: rotate(359deg); + } +} +@keyframes spin { + 0% { + -webkit-transform: rotate(0deg); + transform: rotate(0deg); + } + 100% { + -webkit-transform: rotate(359deg); + transform: rotate(359deg); + } +} +.fa-rotate-90 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=1); + -webkit-transform: rotate(90deg); + -moz-transform: rotate(90deg); + -ms-transform: rotate(90deg); + -o-transform: rotate(90deg); + transform: rotate(90deg); +} +.fa-rotate-180 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2); + -webkit-transform: rotate(180deg); + -moz-transform: rotate(180deg); + -ms-transform: rotate(180deg); + -o-transform: rotate(180deg); + transform: rotate(180deg); +} +.fa-rotate-270 { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=3); + -webkit-transform: rotate(270deg); + -moz-transform: rotate(270deg); + -ms-transform: rotate(270deg); + -o-transform: rotate(270deg); + transform: rotate(270deg); +} +.fa-flip-horizontal { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1); + -webkit-transform: scale(-1, 1); + -moz-transform: scale(-1, 1); + -ms-transform: scale(-1, 1); + -o-transform: scale(-1, 1); + transform: scale(-1, 1); +} +.fa-flip-vertical { + filter: progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1); + -webkit-transform: scale(1, -1); + -moz-transform: scale(1, -1); + -ms-transform: scale(1, -1); + -o-transform: scale(1, -1); + transform: scale(1, -1); +} +.fa-stack { + position: relative; + display: inline-block; + width: 2em; + height: 2em; + line-height: 2em; + vertical-align: middle; +} +.fa-stack-1x, +.fa-stack-2x { + position: absolute; + left: 0; + width: 100%; + text-align: center; +} +.fa-stack-1x { + line-height: inherit; +} +.fa-stack-2x { + font-size: 2em; +} +.fa-inverse { + color: #ffffff; +} +/* Font Awesome uses the Unicode Private Use Area (PUA) to ensure screen + readers do not read off random characters that represent icons */ +.fa-glass:before { + content: "\f000"; +} +.fa-music:before { + content: "\f001"; +} +.fa-search:before { + content: "\f002"; +} +.fa-envelope-o:before { + content: "\f003"; +} +.fa-heart:before { + content: "\f004"; +} +.fa-star:before { + content: "\f005"; +} +.fa-star-o:before { + content: "\f006"; +} +.fa-user:before { + content: "\f007"; +} +.fa-film:before { + content: "\f008"; +} +.fa-th-large:before { + content: "\f009"; +} +.fa-th:before { + content: "\f00a"; +} +.fa-th-list:before { + content: "\f00b"; +} +.fa-check:before { + content: "\f00c"; +} +.fa-times:before { + content: "\f00d"; +} +.fa-search-plus:before { + content: "\f00e"; +} +.fa-search-minus:before { + content: "\f010"; +} +.fa-power-off:before { + content: "\f011"; +} +.fa-signal:before { + content: "\f012"; +} +.fa-gear:before, +.fa-cog:before { + content: "\f013"; +} +.fa-trash-o:before { + content: "\f014"; +} +.fa-home:before { + content: "\f015"; +} +.fa-file-o:before { + content: "\f016"; +} +.fa-clock-o:before { + content: "\f017"; +} +.fa-road:before { + content: "\f018"; +} +.fa-download:before { + content: "\f019"; +} +.fa-arrow-circle-o-down:before { + content: "\f01a"; +} +.fa-arrow-circle-o-up:before { + content: "\f01b"; +} +.fa-inbox:before { + content: "\f01c"; +} +.fa-play-circle-o:before { + content: "\f01d"; +} +.fa-rotate-right:before, +.fa-repeat:before { + content: "\f01e"; +} +.fa-refresh:before { + content: "\f021"; +} +.fa-list-alt:before { + content: "\f022"; +} +.fa-lock:before { + content: "\f023"; +} +.fa-flag:before { + content: "\f024"; +} +.fa-headphones:before { + content: "\f025"; +} +.fa-volume-off:before { + content: "\f026"; +} +.fa-volume-down:before { + content: "\f027"; +} +.fa-volume-up:before { + content: "\f028"; +} +.fa-qrcode:before { + content: "\f029"; +} +.fa-barcode:before { + content: "\f02a"; +} +.fa-tag:before { + content: "\f02b"; +} +.fa-tags:before { + content: "\f02c"; +} +.fa-book:before { + content: "\f02d"; +} +.fa-bookmark:before { + content: "\f02e"; +} +.fa-print:before { + content: "\f02f"; +} +.fa-camera:before { + content: "\f030"; +} +.fa-font:before { + content: "\f031"; +} +.fa-bold:before { + content: "\f032"; +} +.fa-italic:before { + content: "\f033"; +} +.fa-text-height:before { + content: "\f034"; +} +.fa-text-width:before { + content: "\f035"; +} +.fa-align-left:before { + content: "\f036"; +} +.fa-align-center:before { + content: "\f037"; +} +.fa-align-right:before { + content: "\f038"; +} +.fa-align-justify:before { + content: "\f039"; +} +.fa-list:before { + content: "\f03a"; +} +.fa-dedent:before, +.fa-outdent:before { + content: "\f03b"; +} +.fa-indent:before { + content: "\f03c"; +} +.fa-video-camera:before { + content: "\f03d"; +} +.fa-photo:before, +.fa-image:before, +.fa-picture-o:before { + content: "\f03e"; +} +.fa-pencil:before { + content: "\f040"; +} +.fa-map-marker:before { + content: "\f041"; +} +.fa-adjust:before { + content: "\f042"; +} +.fa-tint:before { + content: "\f043"; +} +.fa-edit:before, +.fa-pencil-square-o:before { + content: "\f044"; +} +.fa-share-square-o:before { + content: "\f045"; +} +.fa-check-square-o:before { + content: "\f046"; +} +.fa-arrows:before { + content: "\f047"; +} +.fa-step-backward:before { + content: "\f048"; +} +.fa-fast-backward:before { + content: "\f049"; +} +.fa-backward:before { + content: "\f04a"; +} +.fa-play:before { + content: "\f04b"; +} +.fa-pause:before { + content: "\f04c"; +} +.fa-stop:before { + content: "\f04d"; +} +.fa-forward:before { + content: "\f04e"; +} +.fa-fast-forward:before { + content: "\f050"; +} +.fa-step-forward:before { + content: "\f051"; +} +.fa-eject:before { + content: "\f052"; +} +.fa-chevron-left:before { + content: "\f053"; +} +.fa-chevron-right:before { + content: "\f054"; +} +.fa-plus-circle:before { + content: "\f055"; +} +.fa-minus-circle:before { + content: "\f056"; +} +.fa-times-circle:before { + content: "\f057"; +} +.fa-check-circle:before { + content: "\f058"; +} +.fa-question-circle:before { + content: "\f059"; +} +.fa-info-circle:before { + content: "\f05a"; +} +.fa-crosshairs:before { + content: "\f05b"; +} +.fa-times-circle-o:before { + content: "\f05c"; +} +.fa-check-circle-o:before { + content: "\f05d"; +} +.fa-ban:before { + content: "\f05e"; +} +.fa-arrow-left:before { + content: "\f060"; +} +.fa-arrow-right:before { + content: "\f061"; +} +.fa-arrow-up:before { + content: "\f062"; +} +.fa-arrow-down:before { + content: "\f063"; +} +.fa-mail-forward:before, +.fa-share:before { + content: "\f064"; +} +.fa-expand:before { + content: "\f065"; +} +.fa-compress:before { + content: "\f066"; +} +.fa-plus:before { + content: "\f067"; +} +.fa-minus:before { + content: "\f068"; +} +.fa-asterisk:before { + content: "\f069"; +} +.fa-exclamation-circle:before { + content: "\f06a"; +} +.fa-gift:before { + content: "\f06b"; +} +.fa-leaf:before { + content: "\f06c"; +} +.fa-fire:before { + content: "\f06d"; +} +.fa-eye:before { + content: "\f06e"; +} +.fa-eye-slash:before { + content: "\f070"; +} +.fa-warning:before, +.fa-exclamation-triangle:before { + content: "\f071"; +} +.fa-plane:before { + content: "\f072"; +} +.fa-calendar:before { + content: "\f073"; +} +.fa-random:before { + content: "\f074"; +} +.fa-comment:before { + content: "\f075"; +} +.fa-magnet:before { + content: "\f076"; +} +.fa-chevron-up:before { + content: "\f077"; +} +.fa-chevron-down:before { + content: "\f078"; +} +.fa-retweet:before { + content: "\f079"; +} +.fa-shopping-cart:before { + content: "\f07a"; +} +.fa-folder:before { + content: "\f07b"; +} +.fa-folder-open:before { + content: "\f07c"; +} +.fa-arrows-v:before { + content: "\f07d"; +} +.fa-arrows-h:before { + content: "\f07e"; +} +.fa-bar-chart-o:before { + content: "\f080"; +} +.fa-twitter-square:before { + content: "\f081"; +} +.fa-facebook-square:before { + content: "\f082"; +} +.fa-camera-retro:before { + content: "\f083"; +} +.fa-key:before { + content: "\f084"; +} +.fa-gears:before, +.fa-cogs:before { + content: "\f085"; +} +.fa-comments:before { + content: "\f086"; +} +.fa-thumbs-o-up:before { + content: "\f087"; +} +.fa-thumbs-o-down:before { + content: "\f088"; +} +.fa-star-half:before { + content: "\f089"; +} +.fa-heart-o:before { + content: "\f08a"; +} +.fa-sign-out:before { + content: "\f08b"; +} +.fa-linkedin-square:before { + content: "\f08c"; +} +.fa-thumb-tack:before { + content: "\f08d"; +} +.fa-external-link:before { + content: "\f08e"; +} +.fa-sign-in:before { + content: "\f090"; +} +.fa-trophy:before { + content: "\f091"; +} +.fa-github-square:before { + content: "\f092"; +} +.fa-upload:before { + content: "\f093"; +} +.fa-lemon-o:before { + content: "\f094"; +} +.fa-phone:before { + content: "\f095"; +} +.fa-square-o:before { + content: "\f096"; +} +.fa-bookmark-o:before { + content: "\f097"; +} +.fa-phone-square:before { + content: "\f098"; +} +.fa-twitter:before { + content: "\f099"; +} +.fa-facebook:before { + content: "\f09a"; +} +.fa-github:before { + content: "\f09b"; +} +.fa-unlock:before { + content: "\f09c"; +} +.fa-credit-card:before { + content: "\f09d"; +} +.fa-rss:before { + content: "\f09e"; +} +.fa-hdd-o:before { + content: "\f0a0"; +} +.fa-bullhorn:before { + content: "\f0a1"; +} +.fa-bell:before { + content: "\f0f3"; +} +.fa-certificate:before { + content: "\f0a3"; +} +.fa-hand-o-right:before { + content: "\f0a4"; +} +.fa-hand-o-left:before { + content: "\f0a5"; +} +.fa-hand-o-up:before { + content: "\f0a6"; +} +.fa-hand-o-down:before { + content: "\f0a7"; +} +.fa-arrow-circle-left:before { + content: "\f0a8"; +} +.fa-arrow-circle-right:before { + content: "\f0a9"; +} +.fa-arrow-circle-up:before { + content: "\f0aa"; +} +.fa-arrow-circle-down:before { + content: "\f0ab"; +} +.fa-globe:before { + content: "\f0ac"; +} +.fa-wrench:before { + content: "\f0ad"; +} +.fa-tasks:before { + content: "\f0ae"; +} +.fa-filter:before { + content: "\f0b0"; +} +.fa-briefcase:before { + content: "\f0b1"; +} +.fa-arrows-alt:before { + content: "\f0b2"; +} +.fa-group:before, +.fa-users:before { + content: "\f0c0"; +} +.fa-chain:before, +.fa-link:before { + content: "\f0c1"; +} +.fa-cloud:before { + content: "\f0c2"; +} +.fa-flask:before { + content: "\f0c3"; +} +.fa-cut:before, +.fa-scissors:before { + content: "\f0c4"; +} +.fa-copy:before, +.fa-files-o:before { + content: "\f0c5"; +} +.fa-paperclip:before { + content: "\f0c6"; +} +.fa-save:before, +.fa-floppy-o:before { + content: "\f0c7"; +} +.fa-square:before { + content: "\f0c8"; +} +.fa-navicon:before, +.fa-reorder:before, +.fa-bars:before { + content: "\f0c9"; +} +.fa-list-ul:before { + content: "\f0ca"; +} +.fa-list-ol:before { + content: "\f0cb"; +} +.fa-strikethrough:before { + content: "\f0cc"; +} +.fa-underline:before { + content: "\f0cd"; +} +.fa-table:before { + content: "\f0ce"; +} +.fa-magic:before { + content: "\f0d0"; +} +.fa-truck:before { + content: "\f0d1"; +} +.fa-pinterest:before { + content: "\f0d2"; +} +.fa-pinterest-square:before { + content: "\f0d3"; +} +.fa-google-plus-square:before { + content: "\f0d4"; +} +.fa-google-plus:before { + content: "\f0d5"; +} +.fa-money:before { + content: "\f0d6"; +} +.fa-caret-down:before { + content: "\f0d7"; +} +.fa-caret-up:before { + content: "\f0d8"; +} +.fa-caret-left:before { + content: "\f0d9"; +} +.fa-caret-right:before { + content: "\f0da"; +} +.fa-columns:before { + content: "\f0db"; +} +.fa-unsorted:before, +.fa-sort:before { + content: "\f0dc"; +} +.fa-sort-down:before, +.fa-sort-desc:before { + content: "\f0dd"; +} +.fa-sort-up:before, +.fa-sort-asc:before { + content: "\f0de"; +} +.fa-envelope:before { + content: "\f0e0"; +} +.fa-linkedin:before { + content: "\f0e1"; +} +.fa-rotate-left:before, +.fa-undo:before { + content: "\f0e2"; +} +.fa-legal:before, +.fa-gavel:before { + content: "\f0e3"; +} +.fa-dashboard:before, +.fa-tachometer:before { + content: "\f0e4"; +} +.fa-comment-o:before { + content: "\f0e5"; +} +.fa-comments-o:before { + content: "\f0e6"; +} +.fa-flash:before, +.fa-bolt:before { + content: "\f0e7"; +} +.fa-sitemap:before { + content: "\f0e8"; +} +.fa-umbrella:before { + content: "\f0e9"; +} +.fa-paste:before, +.fa-clipboard:before { + content: "\f0ea"; +} +.fa-lightbulb-o:before { + content: "\f0eb"; +} +.fa-exchange:before { + content: "\f0ec"; +} +.fa-cloud-download:before { + content: "\f0ed"; +} +.fa-cloud-upload:before { + content: "\f0ee"; +} +.fa-user-md:before { + content: "\f0f0"; +} +.fa-stethoscope:before { + content: "\f0f1"; +} +.fa-suitcase:before { + content: "\f0f2"; +} +.fa-bell-o:before { + content: "\f0a2"; +} +.fa-coffee:before { + content: "\f0f4"; +} +.fa-cutlery:before { + content: "\f0f5"; +} +.fa-file-text-o:before { + content: "\f0f6"; +} +.fa-building-o:before { + content: "\f0f7"; +} +.fa-hospital-o:before { + content: "\f0f8"; +} +.fa-ambulance:before { + content: "\f0f9"; +} +.fa-medkit:before { + content: "\f0fa"; +} +.fa-fighter-jet:before { + content: "\f0fb"; +} +.fa-beer:before { + content: "\f0fc"; +} +.fa-h-square:before { + content: "\f0fd"; +} +.fa-plus-square:before { + content: "\f0fe"; +} +.fa-angle-double-left:before { + content: "\f100"; +} +.fa-angle-double-right:before { + content: "\f101"; +} +.fa-angle-double-up:before { + content: "\f102"; +} +.fa-angle-double-down:before { + content: "\f103"; +} +.fa-angle-left:before { + content: "\f104"; +} +.fa-angle-right:before { + content: "\f105"; +} +.fa-angle-up:before { + content: "\f106"; +} +.fa-angle-down:before { + content: "\f107"; +} +.fa-desktop:before { + content: "\f108"; +} +.fa-laptop:before { + content: "\f109"; +} +.fa-tablet:before { + content: "\f10a"; +} +.fa-mobile-phone:before, +.fa-mobile:before { + content: "\f10b"; +} +.fa-circle-o:before { + content: "\f10c"; +} +.fa-quote-left:before { + content: "\f10d"; +} +.fa-quote-right:before { + content: "\f10e"; +} +.fa-spinner:before { + content: "\f110"; +} +.fa-circle:before { + content: "\f111"; +} +.fa-mail-reply:before, +.fa-reply:before { + content: "\f112"; +} +.fa-github-alt:before { + content: "\f113"; +} +.fa-folder-o:before { + content: "\f114"; +} +.fa-folder-open-o:before { + content: "\f115"; +} +.fa-smile-o:before { + content: "\f118"; +} +.fa-frown-o:before { + content: "\f119"; +} +.fa-meh-o:before { + content: "\f11a"; +} +.fa-gamepad:before { + content: "\f11b"; +} +.fa-keyboard-o:before { + content: "\f11c"; +} +.fa-flag-o:before { + content: "\f11d"; +} +.fa-flag-checkered:before { + content: "\f11e"; +} +.fa-terminal:before { + content: "\f120"; +} +.fa-code:before { + content: "\f121"; +} +.fa-mail-reply-all:before, +.fa-reply-all:before { + content: "\f122"; +} +.fa-star-half-empty:before, +.fa-star-half-full:before, +.fa-star-half-o:before { + content: "\f123"; +} +.fa-location-arrow:before { + content: "\f124"; +} +.fa-crop:before { + content: "\f125"; +} +.fa-code-fork:before { + content: "\f126"; +} +.fa-unlink:before, +.fa-chain-broken:before { + content: "\f127"; +} +.fa-question:before { + content: "\f128"; +} +.fa-info:before { + content: "\f129"; +} +.fa-exclamation:before { + content: "\f12a"; +} +.fa-superscript:before { + content: "\f12b"; +} +.fa-subscript:before { + content: "\f12c"; +} +.fa-eraser:before { + content: "\f12d"; +} +.fa-puzzle-piece:before { + content: "\f12e"; +} +.fa-microphone:before { + content: "\f130"; +} +.fa-microphone-slash:before { + content: "\f131"; +} +.fa-shield:before { + content: "\f132"; +} +.fa-calendar-o:before { + content: "\f133"; +} +.fa-fire-extinguisher:before { + content: "\f134"; +} +.fa-rocket:before { + content: "\f135"; +} +.fa-maxcdn:before { + content: "\f136"; +} +.fa-chevron-circle-left:before { + content: "\f137"; +} +.fa-chevron-circle-right:before { + content: "\f138"; +} +.fa-chevron-circle-up:before { + content: "\f139"; +} +.fa-chevron-circle-down:before { + content: "\f13a"; +} +.fa-html5:before { + content: "\f13b"; +} +.fa-css3:before { + content: "\f13c"; +} +.fa-anchor:before { + content: "\f13d"; +} +.fa-unlock-alt:before { + content: "\f13e"; +} +.fa-bullseye:before { + content: "\f140"; +} +.fa-ellipsis-h:before { + content: "\f141"; +} +.fa-ellipsis-v:before { + content: "\f142"; +} +.fa-rss-square:before { + content: "\f143"; +} +.fa-play-circle:before { + content: "\f144"; +} +.fa-ticket:before { + content: "\f145"; +} +.fa-minus-square:before { + content: "\f146"; +} +.fa-minus-square-o:before { + content: "\f147"; +} +.fa-level-up:before { + content: "\f148"; +} +.fa-level-down:before { + content: "\f149"; +} +.fa-check-square:before { + content: "\f14a"; +} +.fa-pencil-square:before { + content: "\f14b"; +} +.fa-external-link-square:before { + content: "\f14c"; +} +.fa-share-square:before { + content: "\f14d"; +} +.fa-compass:before { + content: "\f14e"; +} +.fa-toggle-down:before, +.fa-caret-square-o-down:before { + content: "\f150"; +} +.fa-toggle-up:before, +.fa-caret-square-o-up:before { + content: "\f151"; +} +.fa-toggle-right:before, +.fa-caret-square-o-right:before { + content: "\f152"; +} +.fa-euro:before, +.fa-eur:before { + content: "\f153"; +} +.fa-gbp:before { + content: "\f154"; +} +.fa-dollar:before, +.fa-usd:before { + content: "\f155"; +} +.fa-rupee:before, +.fa-inr:before { + content: "\f156"; +} +.fa-cny:before, +.fa-rmb:before, +.fa-yen:before, +.fa-jpy:before { + content: "\f157"; +} +.fa-ruble:before, +.fa-rouble:before, +.fa-rub:before { + content: "\f158"; +} +.fa-won:before, +.fa-krw:before { + content: "\f159"; +} +.fa-bitcoin:before, +.fa-btc:before { + content: "\f15a"; +} +.fa-file:before { + content: "\f15b"; +} +.fa-file-text:before { + content: "\f15c"; +} +.fa-sort-alpha-asc:before { + content: "\f15d"; +} +.fa-sort-alpha-desc:before { + content: "\f15e"; +} +.fa-sort-amount-asc:before { + content: "\f160"; +} +.fa-sort-amount-desc:before { + content: "\f161"; +} +.fa-sort-numeric-asc:before { + content: "\f162"; +} +.fa-sort-numeric-desc:before { + content: "\f163"; +} +.fa-thumbs-up:before { + content: "\f164"; +} +.fa-thumbs-down:before { + content: "\f165"; +} +.fa-youtube-square:before { + content: "\f166"; +} +.fa-youtube:before { + content: "\f167"; +} +.fa-xing:before { + content: "\f168"; +} +.fa-xing-square:before { + content: "\f169"; +} +.fa-youtube-play:before { + content: "\f16a"; +} +.fa-dropbox:before { + content: "\f16b"; +} +.fa-stack-overflow:before { + content: "\f16c"; +} +.fa-instagram:before { + content: "\f16d"; +} +.fa-flickr:before { + content: "\f16e"; +} +.fa-adn:before { + content: "\f170"; +} +.fa-bitbucket:before { + content: "\f171"; +} +.fa-bitbucket-square:before { + content: "\f172"; +} +.fa-tumblr:before { + content: "\f173"; +} +.fa-tumblr-square:before { + content: "\f174"; +} +.fa-long-arrow-down:before { + content: "\f175"; +} +.fa-long-arrow-up:before { + content: "\f176"; +} +.fa-long-arrow-left:before { + content: "\f177"; +} +.fa-long-arrow-right:before { + content: "\f178"; +} +.fa-apple:before { + content: "\f179"; +} +.fa-windows:before { + content: "\f17a"; +} +.fa-android:before { + content: "\f17b"; +} +.fa-linux:before { + content: "\f17c"; +} +.fa-dribbble:before { + content: "\f17d"; +} +.fa-skype:before { + content: "\f17e"; +} +.fa-foursquare:before { + content: "\f180"; +} +.fa-trello:before { + content: "\f181"; +} +.fa-female:before { + content: "\f182"; +} +.fa-male:before { + content: "\f183"; +} +.fa-gittip:before { + content: "\f184"; +} +.fa-sun-o:before { + content: "\f185"; +} +.fa-moon-o:before { + content: "\f186"; +} +.fa-archive:before { + content: "\f187"; +} +.fa-bug:before { + content: "\f188"; +} +.fa-vk:before { + content: "\f189"; +} +.fa-weibo:before { + content: "\f18a"; +} +.fa-renren:before { + content: "\f18b"; +} +.fa-pagelines:before { + content: "\f18c"; +} +.fa-stack-exchange:before { + content: "\f18d"; +} +.fa-arrow-circle-o-right:before { + content: "\f18e"; +} +.fa-arrow-circle-o-left:before { + content: "\f190"; +} +.fa-toggle-left:before, +.fa-caret-square-o-left:before { + content: "\f191"; +} +.fa-dot-circle-o:before { + content: "\f192"; +} +.fa-wheelchair:before { + content: "\f193"; +} +.fa-vimeo-square:before { + content: "\f194"; +} +.fa-turkish-lira:before, +.fa-try:before { + content: "\f195"; +} +.fa-plus-square-o:before { + content: "\f196"; +} +.fa-space-shuttle:before { + content: "\f197"; +} +.fa-slack:before { + content: "\f198"; +} +.fa-envelope-square:before { + content: "\f199"; +} +.fa-wordpress:before { + content: "\f19a"; +} +.fa-openid:before { + content: "\f19b"; +} +.fa-institution:before, +.fa-bank:before, +.fa-university:before { + content: "\f19c"; +} +.fa-mortar-board:before, +.fa-graduation-cap:before { + content: "\f19d"; +} +.fa-yahoo:before { + content: "\f19e"; +} +.fa-google:before { + content: "\f1a0"; +} +.fa-reddit:before { + content: "\f1a1"; +} +.fa-reddit-square:before { + content: "\f1a2"; +} +.fa-stumbleupon-circle:before { + content: "\f1a3"; +} +.fa-stumbleupon:before { + content: "\f1a4"; +} +.fa-delicious:before { + content: "\f1a5"; +} +.fa-digg:before { + content: "\f1a6"; +} +.fa-pied-piper-square:before, +.fa-pied-piper:before { + content: "\f1a7"; +} +.fa-pied-piper-alt:before { + content: "\f1a8"; +} +.fa-drupal:before { + content: "\f1a9"; +} +.fa-joomla:before { + content: "\f1aa"; +} +.fa-language:before { + content: "\f1ab"; +} +.fa-fax:before { + content: "\f1ac"; +} +.fa-building:before { + content: "\f1ad"; +} +.fa-child:before { + content: "\f1ae"; +} +.fa-paw:before { + content: "\f1b0"; +} +.fa-spoon:before { + content: "\f1b1"; +} +.fa-cube:before { + content: "\f1b2"; +} +.fa-cubes:before { + content: "\f1b3"; +} +.fa-behance:before { + content: "\f1b4"; +} +.fa-behance-square:before { + content: "\f1b5"; +} +.fa-steam:before { + content: "\f1b6"; +} +.fa-steam-square:before { + content: "\f1b7"; +} +.fa-recycle:before { + content: "\f1b8"; +} +.fa-automobile:before, +.fa-car:before { + content: "\f1b9"; +} +.fa-cab:before, +.fa-taxi:before { + content: "\f1ba"; +} +.fa-tree:before { + content: "\f1bb"; +} +.fa-spotify:before { + content: "\f1bc"; +} +.fa-deviantart:before { + content: "\f1bd"; +} +.fa-soundcloud:before { + content: "\f1be"; +} +.fa-database:before { + content: "\f1c0"; +} +.fa-file-pdf-o:before { + content: "\f1c1"; +} +.fa-file-word-o:before { + content: "\f1c2"; +} +.fa-file-excel-o:before { + content: "\f1c3"; +} +.fa-file-powerpoint-o:before { + content: "\f1c4"; +} +.fa-file-photo-o:before, +.fa-file-picture-o:before, +.fa-file-image-o:before { + content: "\f1c5"; +} +.fa-file-zip-o:before, +.fa-file-archive-o:before { + content: "\f1c6"; +} +.fa-file-sound-o:before, +.fa-file-audio-o:before { + content: "\f1c7"; +} +.fa-file-movie-o:before, +.fa-file-video-o:before { + content: "\f1c8"; +} +.fa-file-code-o:before { + content: "\f1c9"; +} +.fa-vine:before { + content: "\f1ca"; +} +.fa-codepen:before { + content: "\f1cb"; +} +.fa-jsfiddle:before { + content: "\f1cc"; +} +.fa-life-bouy:before, +.fa-life-saver:before, +.fa-support:before, +.fa-life-ring:before { + content: "\f1cd"; +} +.fa-circle-o-notch:before { + content: "\f1ce"; +} +.fa-ra:before, +.fa-rebel:before { + content: "\f1d0"; +} +.fa-ge:before, +.fa-empire:before { + content: "\f1d1"; +} +.fa-git-square:before { + content: "\f1d2"; +} +.fa-git:before { + content: "\f1d3"; +} +.fa-hacker-news:before { + content: "\f1d4"; +} +.fa-tencent-weibo:before { + content: "\f1d5"; +} +.fa-qq:before { + content: "\f1d6"; +} +.fa-wechat:before, +.fa-weixin:before { + content: "\f1d7"; +} +.fa-send:before, +.fa-paper-plane:before { + content: "\f1d8"; +} +.fa-send-o:before, +.fa-paper-plane-o:before { + content: "\f1d9"; +} +.fa-history:before { + content: "\f1da"; +} +.fa-circle-thin:before { + content: "\f1db"; +} +.fa-header:before { + content: "\f1dc"; +} +.fa-paragraph:before { + content: "\f1dd"; +} +.fa-sliders:before { + content: "\f1de"; +} +.fa-share-alt:before { + content: "\f1e0"; +} +.fa-share-alt-square:before { + content: "\f1e1"; +} +.fa-bomb:before { + content: "\f1e2"; +} diff --git a/app/assets/stylesheets/boot/google-webfonts.css.scss b/app/assets/stylesheets/boot/google-webfonts.css.scss new file mode 100644 index 00000000..7e543438 --- /dev/null +++ b/app/assets/stylesheets/boot/google-webfonts.css.scss @@ -0,0 +1,48 @@ +// License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> + +/* Open Sans */ + +@font-face { + font-family: "Open Sans"; + src: font-url("Open_Sans/opensans-regular-webfont.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Open Sans"; + src: font-url("Open_Sans/opensans-light-webfont.woff") format("woff"); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: "Open Sans"; + src: font-url("Open_Sans/opensans-bold-webfont.woff") format("woff"); + font-weight: bold; + font-style: normal; +} + +/* Bitter */ + +@font-face { + font-family: "OpenSansCondensed"; + src: font-url("Open_Sans_Condensed/opensans-condbold-webfont.woff") + format("woff"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Bitter"; + src: font-url("Bitter/Bitter-Regular.woff") format("woff"); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: "Bitter"; + src: font-url("Bitter/Bitter-Bold.woff") format("woff"); + font-weight: bold; + font-style: normal; +} From 4645f8d1d0436004d85426d53bde4dc844b4be3b Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 11 Feb 2020 12:55:57 -0600 Subject: [PATCH 272/440] Correct typo in super admin --- app/views/super_admins/index.html.erb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/super_admins/index.html.erb b/app/views/super_admins/index.html.erb index b9dcb8ba..c306e6d3 100644 --- a/app/views/super_admins/index.html.erb +++ b/app/views/super_admins/index.html.erb @@ -1,6 +1,6 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> <%= content_for :javascripts do %> - <%= <%= javascript_pack_tag 'i18n', 'page__super-admin' %> + <%= javascript_pack_tag 'i18n', 'page__super-admin' %> <% end %> <% content_for(:hide_nav_beacon) {'true'} %> From 49bdabe88953de1711391f1452cf21128e4537b4 Mon Sep 17 00:00:00 2001 From: Eric Schultz Date: Tue, 11 Feb 2020 12:57:50 -0600 Subject: [PATCH 273/440] Fix typo in stylesheet --- app/views/campaigns/show.html.erb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/views/campaigns/show.html.erb b/app/views/campaigns/show.html.erb index c8fb5511..1bea0da4 100644 --- a/app/views/campaigns/show.html.erb +++ b/app/views/campaigns/show.html.erb @@ -32,7 +32,6 @@ <%= stylesheet_link_tag 'campaigns/show/page' %> <%= stylesheet_link_tag 'campaigns/edit/page' %> + + + + <%%= yield %> + + diff --git a/lib/templates/erb/mailer/layout.text.erb.tt b/lib/templates/erb/mailer/layout.text.erb.tt new file mode 100644 index 00000000..b4bae3aa --- /dev/null +++ b/lib/templates/erb/mailer/layout.text.erb.tt @@ -0,0 +1,2 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +<%%= yield %> diff --git a/lib/templates/erb/mailer/view.html.erb.tt b/lib/templates/erb/mailer/view.html.erb.tt new file mode 100644 index 00000000..a430ca69 --- /dev/null +++ b/lib/templates/erb/mailer/view.html.erb.tt @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

<%= class_name %>#<%= @action %>

+ +

+ <%%= @greeting %>, find me in <%= @path %> +

diff --git a/lib/templates/erb/mailer/view.text.erb.tt b/lib/templates/erb/mailer/view.text.erb.tt new file mode 100644 index 00000000..4e048e81 --- /dev/null +++ b/lib/templates/erb/mailer/view.text.erb.tt @@ -0,0 +1,4 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +<%= class_name %>#<%= @action %> + +<%%= @greeting %>, find me in <%= @path %> diff --git a/lib/templates/erb/scaffold/_form.html.erb.tt b/lib/templates/erb/scaffold/_form.html.erb.tt new file mode 100644 index 00000000..cca7eb77 --- /dev/null +++ b/lib/templates/erb/scaffold/_form.html.erb.tt @@ -0,0 +1,35 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +<%%= form_with(model: <%= model_resource_name %>, local: true) do |form| %> + <%% if <%= singular_table_name %>.errors.any? %> +
+

<%%= pluralize(<%= singular_table_name %>.errors.count, "error") %> prohibited this <%= singular_table_name %> from being saved:

+ +
    + <%% <%= singular_table_name %>.errors.full_messages.each do |message| %> +
  • <%%= message %>
  • + <%% end %> +
+
+ <%% end %> + +<% attributes.each do |attribute| -%> +
+<% if attribute.password_digest? -%> + <%%= form.label :password %> + <%%= form.password_field :password %> +
+ +
+ <%%= form.label :password_confirmation %> + <%%= form.password_field :password_confirmation %> +<% else -%> + <%%= form.label :<%= attribute.column_name %> %> + <%%= form.<%= attribute.field_type %> :<%= attribute.column_name %> %> +<% end -%> +
+ +<% end -%> +
+ <%%= form.submit %> +
+<%% end %> diff --git a/lib/templates/erb/scaffold/edit.html.erb.tt b/lib/templates/erb/scaffold/edit.html.erb.tt new file mode 100644 index 00000000..cc231a9e --- /dev/null +++ b/lib/templates/erb/scaffold/edit.html.erb.tt @@ -0,0 +1,7 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

Editing <%= singular_table_name.titleize %>

+ +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> + +<%%= link_to 'Show', @<%= singular_table_name %> %> | +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/templates/erb/scaffold/index.html.erb.tt b/lib/templates/erb/scaffold/index.html.erb.tt new file mode 100644 index 00000000..bfc59d77 --- /dev/null +++ b/lib/templates/erb/scaffold/index.html.erb.tt @@ -0,0 +1,32 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

<%%= notice %>

+ +

<%= plural_table_name.titleize %>

+ + + + +<% attributes.reject(&:password_digest?).each do |attribute| -%> + +<% end -%> + + + + + + <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %> + +<% attributes.reject(&:password_digest?).each do |attribute| -%> + +<% end -%> + + + + + <%% end %> + +
<%= attribute.human_name %>
<%%= <%= singular_table_name %>.<%= attribute.name %> %><%%= link_to 'Show', <%= model_resource_name %> %><%%= link_to 'Edit', edit_<%= singular_route_name %>_path(<%= singular_table_name %>) %><%%= link_to 'Destroy', <%= model_resource_name %>, method: :delete, data: { confirm: 'Are you sure?' } %>
+ +
+ +<%%= link_to 'New <%= singular_table_name.titleize %>', new_<%= singular_route_name %>_path %> diff --git a/lib/templates/erb/scaffold/new.html.erb.tt b/lib/templates/erb/scaffold/new.html.erb.tt new file mode 100644 index 00000000..9ed89340 --- /dev/null +++ b/lib/templates/erb/scaffold/new.html.erb.tt @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

New <%= singular_table_name.titleize %>

+ +<%%= render 'form', <%= singular_table_name %>: @<%= singular_table_name %> %> + +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/templates/erb/scaffold/show.html.erb.tt b/lib/templates/erb/scaffold/show.html.erb.tt new file mode 100644 index 00000000..e938f045 --- /dev/null +++ b/lib/templates/erb/scaffold/show.html.erb.tt @@ -0,0 +1,12 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

<%%= notice %>

+ +<% attributes.reject(&:password_digest?).each do |attribute| -%> +

+ <%= attribute.human_name %>: + <%%= @<%= singular_table_name %>.<%= attribute.name %> %> +

+ +<% end -%> +<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> | +<%%= link_to 'Back', <%= index_helper %>_path %> diff --git a/lib/templates/rails/assets/javascript.js b/lib/templates/rails/assets/javascript.js new file mode 100644 index 00000000..80d5ae04 --- /dev/null +++ b/lib/templates/rails/assets/javascript.js @@ -0,0 +1,3 @@ +// License: LGPL-3.0-or-later +// Place all the behaviors and hooks related to the matching controller here. +// All this logic will automatically be available in application.js. diff --git a/lib/templates/rails/assets/stylesheet.css b/lib/templates/rails/assets/stylesheet.css new file mode 100644 index 00000000..72b4231a --- /dev/null +++ b/lib/templates/rails/assets/stylesheet.css @@ -0,0 +1,5 @@ +/* License: LGPL-3.0-or-later */ +/* + Place all the styles related to the matching controller here. + They will automatically be included in application.css. +*/ diff --git a/lib/templates/rails/scaffold_controller/api_controller.rb.tt b/lib/templates/rails/scaffold_controller/api_controller.rb.tt new file mode 100644 index 00000000..e8f83972 --- /dev/null +++ b/lib/templates/rails/scaffold_controller/api_controller.rb.tt @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +<% if namespaced? -%> +require_dependency "<%= namespaced_path %>/application_controller" + +<% end -%> +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: [:show, :update, :destroy] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + + render json: <%= "@#{plural_table_name}" %> + end + + # GET <%= route_url %>/1 + def show + render json: <%= "@#{singular_table_name}" %> + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + render json: <%= "@#{singular_table_name}" %> + else + render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + end + + # Only allow a trusted parameter "white list" through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params.fetch(:<%= singular_table_name %>, {}) + <%- else -%> + params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + <%- end -%> + end +end +<% end -%> diff --git a/lib/templates/rails/scaffold_controller/controller.rb.tt b/lib/templates/rails/scaffold_controller/controller.rb.tt new file mode 100644 index 00000000..a25273b7 --- /dev/null +++ b/lib/templates/rails/scaffold_controller/controller.rb.tt @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +<% if namespaced? -%> +require_dependency "<%= namespaced_path %>/application_controller" + +<% end -%> +<% module_namespacing do -%> +class <%= controller_class_name %>Controller < ApplicationController + before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy] + + # GET <%= route_url %> + def index + @<%= plural_table_name %> = <%= orm_class.all(class_name) %> + end + + # GET <%= route_url %>/1 + def show + end + + # GET <%= route_url %>/new + def new + @<%= singular_table_name %> = <%= orm_class.build(class_name) %> + end + + # GET <%= route_url %>/1/edit + def edit + end + + # POST <%= route_url %> + def create + @<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %> + + if @<%= orm_instance.save %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully created.'" %> + else + render :new + end + end + + # PATCH/PUT <%= route_url %>/1 + def update + if @<%= orm_instance.update("#{singular_table_name}_params") %> + redirect_to <%= redirect_resource_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> + else + render :edit + end + end + + # DELETE <%= route_url %>/1 + def destroy + @<%= orm_instance.destroy %> + redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %> + end + + private + # Use callbacks to share common setup or constraints between actions. + def set_<%= singular_table_name %> + @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %> + end + + # Only allow a trusted parameter "white list" through. + def <%= "#{singular_table_name}_params" %> + <%- if attributes_names.empty? -%> + params.fetch(:<%= singular_table_name %>, {}) + <%- else -%> + params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>) + <%- end -%> + end +end +<% end -%> From c524fcf3c7ea5c1f92d8b9f2ee23f2529dcf450f Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 13:04:03 -0500 Subject: [PATCH 308/440] Add task template --- lib/templates/rails/task/task.rb.tt | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 lib/templates/rails/task/task.rb.tt diff --git a/lib/templates/rails/task/task.rb.tt b/lib/templates/rails/task/task.rb.tt new file mode 100644 index 00000000..ba85a077 --- /dev/null +++ b/lib/templates/rails/task/task.rb.tt @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +namespace :<%= file_name %> do +<% actions.each do |action| -%> + desc "TODO" + task <%= action %>: :environment do + end + +<% end -%> +end From 9227907b260bc486c962b34138d7dd7f9ae9ed58 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 13:51:23 -0500 Subject: [PATCH 309/440] Move generators which override part of rails into their own place --- lib/generators/overrides.rb | 4 ++++ lib/generators/{ => overrides}/rails/plugin/USAGE | 0 lib/generators/{ => overrides}/rails/plugin/plugin_builder.rb | 0 .../{ => overrides}/rails/plugin/templates/%name%.gemspec.tt | 0 .../{ => overrides}/rails/plugin/templates/AGPL-3.0.txt.tt | 0 .../{ => overrides}/rails/plugin/templates/GPL-3.0.txt.tt | 0 .../{ => overrides}/rails/plugin/templates/Gemfile.tt | 0 .../{ => overrides}/rails/plugin/templates/LGPL-3.0.txt.tt | 0 .../{ => overrides}/rails/plugin/templates/LICENSE.tt | 0 .../{ => overrides}/rails/plugin/templates/README.md.tt | 0 .../{ => overrides}/rails/plugin/templates/Rakefile.tt | 0 .../%namespaced_name%/application_controller.rb.tt | 0 .../app/helpers/%namespaced_name%/application_helper.rb.tt | 0 .../app/jobs/%namespaced_name%/application_job.rb.tt | 0 .../app/mailers/%namespaced_name%/application_mailer.rb.tt | 0 .../app/models/%namespaced_name%/application_record.rb.tt | 0 .../views/layouts/%namespaced_name%/application.html.erb.tt | 0 .../{ => overrides}/rails/plugin/templates/bin/rails.tt | 0 .../{ => overrides}/rails/plugin/templates/bin/test.tt | 0 .../rails/plugin/templates/config/routes.rb.tt | 0 .../{ => overrides}/rails/plugin/templates/gitignore.tt | 0 .../rails/plugin/templates/lib/%namespaced_name%.rb.tt | 0 .../rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt | 0 .../plugin/templates/lib/%namespaced_name%/railtie.rb.tt | 0 .../plugin/templates/lib/%namespaced_name%/version.rb.tt | 0 .../templates/lib/tasks/%namespaced_name%_tasks.rake.tt | 0 .../rails/plugin/templates/rails/application.rb.tt | 0 .../{ => overrides}/rails/plugin/templates/rails/boot.rb.tt | 0 .../rails/plugin/templates/rails/dummy_manifest.js.tt | 0 .../rails/plugin/templates/rails/engine_manifest.js.tt | 0 .../rails/plugin/templates/rails/javascripts.js.tt | 0 .../{ => overrides}/rails/plugin/templates/rails/routes.rb.tt | 0 .../rails/plugin/templates/rails/stylesheets.css | 0 .../rails/plugin/templates/test/%namespaced_name%_test.rb.tt | 0 .../plugin/templates/test/application_system_test_case.rb.tt | 0 .../plugin/templates/test/integration/navigation_test.rb.tt | 0 .../rails/plugin/templates/test/test_helper.rb.tt | 0 37 files changed, 4 insertions(+) create mode 100644 lib/generators/overrides.rb rename lib/generators/{ => overrides}/rails/plugin/USAGE (100%) rename lib/generators/{ => overrides}/rails/plugin/plugin_builder.rb (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/%name%.gemspec.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/AGPL-3.0.txt.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/GPL-3.0.txt.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/Gemfile.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/LGPL-3.0.txt.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/LICENSE.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/README.md.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/Rakefile.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/bin/rails.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/bin/test.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/config/routes.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/gitignore.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/lib/%namespaced_name%.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/application.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/boot.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/dummy_manifest.js.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/engine_manifest.js.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/javascripts.js.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/routes.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/rails/stylesheets.css (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/test/%namespaced_name%_test.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/test/application_system_test_case.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/test/integration/navigation_test.rb.tt (100%) rename lib/generators/{ => overrides}/rails/plugin/templates/test/test_helper.rb.tt (100%) diff --git a/lib/generators/overrides.rb b/lib/generators/overrides.rb new file mode 100644 index 00000000..f4570846 --- /dev/null +++ b/lib/generators/overrides.rb @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require_relative './overrides/rails/plugin/plugin_builder' \ No newline at end of file diff --git a/lib/generators/rails/plugin/USAGE b/lib/generators/overrides/rails/plugin/USAGE similarity index 100% rename from lib/generators/rails/plugin/USAGE rename to lib/generators/overrides/rails/plugin/USAGE diff --git a/lib/generators/rails/plugin/plugin_builder.rb b/lib/generators/overrides/rails/plugin/plugin_builder.rb similarity index 100% rename from lib/generators/rails/plugin/plugin_builder.rb rename to lib/generators/overrides/rails/plugin/plugin_builder.rb diff --git a/lib/generators/rails/plugin/templates/%name%.gemspec.tt b/lib/generators/overrides/rails/plugin/templates/%name%.gemspec.tt similarity index 100% rename from lib/generators/rails/plugin/templates/%name%.gemspec.tt rename to lib/generators/overrides/rails/plugin/templates/%name%.gemspec.tt diff --git a/lib/generators/rails/plugin/templates/AGPL-3.0.txt.tt b/lib/generators/overrides/rails/plugin/templates/AGPL-3.0.txt.tt similarity index 100% rename from lib/generators/rails/plugin/templates/AGPL-3.0.txt.tt rename to lib/generators/overrides/rails/plugin/templates/AGPL-3.0.txt.tt diff --git a/lib/generators/rails/plugin/templates/GPL-3.0.txt.tt b/lib/generators/overrides/rails/plugin/templates/GPL-3.0.txt.tt similarity index 100% rename from lib/generators/rails/plugin/templates/GPL-3.0.txt.tt rename to lib/generators/overrides/rails/plugin/templates/GPL-3.0.txt.tt diff --git a/lib/generators/rails/plugin/templates/Gemfile.tt b/lib/generators/overrides/rails/plugin/templates/Gemfile.tt similarity index 100% rename from lib/generators/rails/plugin/templates/Gemfile.tt rename to lib/generators/overrides/rails/plugin/templates/Gemfile.tt diff --git a/lib/generators/rails/plugin/templates/LGPL-3.0.txt.tt b/lib/generators/overrides/rails/plugin/templates/LGPL-3.0.txt.tt similarity index 100% rename from lib/generators/rails/plugin/templates/LGPL-3.0.txt.tt rename to lib/generators/overrides/rails/plugin/templates/LGPL-3.0.txt.tt diff --git a/lib/generators/rails/plugin/templates/LICENSE.tt b/lib/generators/overrides/rails/plugin/templates/LICENSE.tt similarity index 100% rename from lib/generators/rails/plugin/templates/LICENSE.tt rename to lib/generators/overrides/rails/plugin/templates/LICENSE.tt diff --git a/lib/generators/rails/plugin/templates/README.md.tt b/lib/generators/overrides/rails/plugin/templates/README.md.tt similarity index 100% rename from lib/generators/rails/plugin/templates/README.md.tt rename to lib/generators/overrides/rails/plugin/templates/README.md.tt diff --git a/lib/generators/rails/plugin/templates/Rakefile.tt b/lib/generators/overrides/rails/plugin/templates/Rakefile.tt similarity index 100% rename from lib/generators/rails/plugin/templates/Rakefile.tt rename to lib/generators/overrides/rails/plugin/templates/Rakefile.tt diff --git a/lib/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt b/lib/generators/overrides/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt rename to lib/generators/overrides/rails/plugin/templates/app/controllers/%namespaced_name%/application_controller.rb.tt diff --git a/lib/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt b/lib/generators/overrides/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt rename to lib/generators/overrides/rails/plugin/templates/app/helpers/%namespaced_name%/application_helper.rb.tt diff --git a/lib/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt b/lib/generators/overrides/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt rename to lib/generators/overrides/rails/plugin/templates/app/jobs/%namespaced_name%/application_job.rb.tt diff --git a/lib/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt b/lib/generators/overrides/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt rename to lib/generators/overrides/rails/plugin/templates/app/mailers/%namespaced_name%/application_mailer.rb.tt diff --git a/lib/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt b/lib/generators/overrides/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt rename to lib/generators/overrides/rails/plugin/templates/app/models/%namespaced_name%/application_record.rb.tt diff --git a/lib/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt b/lib/generators/overrides/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt rename to lib/generators/overrides/rails/plugin/templates/app/views/layouts/%namespaced_name%/application.html.erb.tt diff --git a/lib/generators/rails/plugin/templates/bin/rails.tt b/lib/generators/overrides/rails/plugin/templates/bin/rails.tt similarity index 100% rename from lib/generators/rails/plugin/templates/bin/rails.tt rename to lib/generators/overrides/rails/plugin/templates/bin/rails.tt diff --git a/lib/generators/rails/plugin/templates/bin/test.tt b/lib/generators/overrides/rails/plugin/templates/bin/test.tt similarity index 100% rename from lib/generators/rails/plugin/templates/bin/test.tt rename to lib/generators/overrides/rails/plugin/templates/bin/test.tt diff --git a/lib/generators/rails/plugin/templates/config/routes.rb.tt b/lib/generators/overrides/rails/plugin/templates/config/routes.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/config/routes.rb.tt rename to lib/generators/overrides/rails/plugin/templates/config/routes.rb.tt diff --git a/lib/generators/rails/plugin/templates/gitignore.tt b/lib/generators/overrides/rails/plugin/templates/gitignore.tt similarity index 100% rename from lib/generators/rails/plugin/templates/gitignore.tt rename to lib/generators/overrides/rails/plugin/templates/gitignore.tt diff --git a/lib/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt b/lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/lib/%namespaced_name%.rb.tt rename to lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%.rb.tt diff --git a/lib/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt b/lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt rename to lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/engine.rb.tt diff --git a/lib/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt b/lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt rename to lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/railtie.rb.tt diff --git a/lib/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt b/lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt rename to lib/generators/overrides/rails/plugin/templates/lib/%namespaced_name%/version.rb.tt diff --git a/lib/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt b/lib/generators/overrides/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt similarity index 100% rename from lib/generators/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt rename to lib/generators/overrides/rails/plugin/templates/lib/tasks/%namespaced_name%_tasks.rake.tt diff --git a/lib/generators/rails/plugin/templates/rails/application.rb.tt b/lib/generators/overrides/rails/plugin/templates/rails/application.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/application.rb.tt rename to lib/generators/overrides/rails/plugin/templates/rails/application.rb.tt diff --git a/lib/generators/rails/plugin/templates/rails/boot.rb.tt b/lib/generators/overrides/rails/plugin/templates/rails/boot.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/boot.rb.tt rename to lib/generators/overrides/rails/plugin/templates/rails/boot.rb.tt diff --git a/lib/generators/rails/plugin/templates/rails/dummy_manifest.js.tt b/lib/generators/overrides/rails/plugin/templates/rails/dummy_manifest.js.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/dummy_manifest.js.tt rename to lib/generators/overrides/rails/plugin/templates/rails/dummy_manifest.js.tt diff --git a/lib/generators/rails/plugin/templates/rails/engine_manifest.js.tt b/lib/generators/overrides/rails/plugin/templates/rails/engine_manifest.js.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/engine_manifest.js.tt rename to lib/generators/overrides/rails/plugin/templates/rails/engine_manifest.js.tt diff --git a/lib/generators/rails/plugin/templates/rails/javascripts.js.tt b/lib/generators/overrides/rails/plugin/templates/rails/javascripts.js.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/javascripts.js.tt rename to lib/generators/overrides/rails/plugin/templates/rails/javascripts.js.tt diff --git a/lib/generators/rails/plugin/templates/rails/routes.rb.tt b/lib/generators/overrides/rails/plugin/templates/rails/routes.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/rails/routes.rb.tt rename to lib/generators/overrides/rails/plugin/templates/rails/routes.rb.tt diff --git a/lib/generators/rails/plugin/templates/rails/stylesheets.css b/lib/generators/overrides/rails/plugin/templates/rails/stylesheets.css similarity index 100% rename from lib/generators/rails/plugin/templates/rails/stylesheets.css rename to lib/generators/overrides/rails/plugin/templates/rails/stylesheets.css diff --git a/lib/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt b/lib/generators/overrides/rails/plugin/templates/test/%namespaced_name%_test.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/test/%namespaced_name%_test.rb.tt rename to lib/generators/overrides/rails/plugin/templates/test/%namespaced_name%_test.rb.tt diff --git a/lib/generators/rails/plugin/templates/test/application_system_test_case.rb.tt b/lib/generators/overrides/rails/plugin/templates/test/application_system_test_case.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/test/application_system_test_case.rb.tt rename to lib/generators/overrides/rails/plugin/templates/test/application_system_test_case.rb.tt diff --git a/lib/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt b/lib/generators/overrides/rails/plugin/templates/test/integration/navigation_test.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/test/integration/navigation_test.rb.tt rename to lib/generators/overrides/rails/plugin/templates/test/integration/navigation_test.rb.tt diff --git a/lib/generators/rails/plugin/templates/test/test_helper.rb.tt b/lib/generators/overrides/rails/plugin/templates/test/test_helper.rb.tt similarity index 100% rename from lib/generators/rails/plugin/templates/test/test_helper.rb.tt rename to lib/generators/overrides/rails/plugin/templates/test/test_helper.rb.tt From cf0078e6d750b5217db56f6899de78df0f63460a Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 13:51:46 -0500 Subject: [PATCH 310/440] Modify bin so it uses our generators over rails --- bin/rails | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/rails b/bin/rails index a31728ab..dd20b7d2 100755 --- a/bin/rails +++ b/bin/rails @@ -3,4 +3,5 @@ APP_PATH = File.expand_path('../config/application', __dir__) require_relative '../config/boot' +require_relative '../lib/generators/overrides' require 'rails/commands' From d37a23152434c853bb9d610d1f063bc13e7ed324 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 14:28:55 -0500 Subject: [PATCH 311/440] Add templates for rspec --- .../rspec/channel/channel_spec.rb.erb | 10 + .../rspec/controller/controller_spec.rb | 19 ++ .../rspec/controller/request_spec.rb | 17 ++ .../rspec/controller/routing_spec.rb | 16 ++ lib/templates/rspec/controller/view_spec.rb | 8 + .../rspec/feature/feature_singular_spec.rb | 8 + lib/templates/rspec/feature/feature_spec.rb | 8 + .../rspec/generator/generator_spec.rb | 9 + lib/templates/rspec/helper/helper_spec.rb | 20 ++ .../rspec/install/spec/rails_helper.rb | 82 ++++++++ .../rspec/integration/request_spec.rb | 13 ++ lib/templates/rspec/job/job_spec.rb.erb | 10 + .../rspec/mailbox/mailbox_spec.rb.erb | 10 + lib/templates/rspec/mailer/fixture | 3 + lib/templates/rspec/mailer/mailer_spec.rb | 28 +++ lib/templates/rspec/mailer/preview.rb | 16 ++ lib/templates/rspec/model/fixtures.yml | 19 ++ lib/templates/rspec/model/model_spec.rb | 10 + .../rspec/scaffold/api_controller_spec.rb | 132 ++++++++++++ .../rspec/scaffold/api_request_spec.rb | 134 ++++++++++++ .../rspec/scaffold/controller_spec.rb | 196 ++++++++++++++++++ lib/templates/rspec/scaffold/edit_spec.rb | 30 +++ lib/templates/rspec/scaffold/index_spec.rb | 28 +++ lib/templates/rspec/scaffold/new_spec.rb | 29 +++ lib/templates/rspec/scaffold/request_spec.rb | 136 ++++++++++++ lib/templates/rspec/scaffold/routing_spec.rb | 49 +++++ lib/templates/rspec/scaffold/show_spec.rb | 24 +++ lib/templates/rspec/system/system_spec.rb | 12 ++ lib/templates/rspec/view/view_spec.rb | 8 + 29 files changed, 1084 insertions(+) create mode 100644 lib/templates/rspec/channel/channel_spec.rb.erb create mode 100644 lib/templates/rspec/controller/controller_spec.rb create mode 100644 lib/templates/rspec/controller/request_spec.rb create mode 100644 lib/templates/rspec/controller/routing_spec.rb create mode 100644 lib/templates/rspec/controller/view_spec.rb create mode 100644 lib/templates/rspec/feature/feature_singular_spec.rb create mode 100644 lib/templates/rspec/feature/feature_spec.rb create mode 100644 lib/templates/rspec/generator/generator_spec.rb create mode 100644 lib/templates/rspec/helper/helper_spec.rb create mode 100644 lib/templates/rspec/install/spec/rails_helper.rb create mode 100644 lib/templates/rspec/integration/request_spec.rb create mode 100644 lib/templates/rspec/job/job_spec.rb.erb create mode 100644 lib/templates/rspec/mailbox/mailbox_spec.rb.erb create mode 100644 lib/templates/rspec/mailer/fixture create mode 100644 lib/templates/rspec/mailer/mailer_spec.rb create mode 100644 lib/templates/rspec/mailer/preview.rb create mode 100644 lib/templates/rspec/model/fixtures.yml create mode 100644 lib/templates/rspec/model/model_spec.rb create mode 100644 lib/templates/rspec/scaffold/api_controller_spec.rb create mode 100644 lib/templates/rspec/scaffold/api_request_spec.rb create mode 100644 lib/templates/rspec/scaffold/controller_spec.rb create mode 100644 lib/templates/rspec/scaffold/edit_spec.rb create mode 100644 lib/templates/rspec/scaffold/index_spec.rb create mode 100644 lib/templates/rspec/scaffold/new_spec.rb create mode 100644 lib/templates/rspec/scaffold/request_spec.rb create mode 100644 lib/templates/rspec/scaffold/routing_spec.rb create mode 100644 lib/templates/rspec/scaffold/show_spec.rb create mode 100644 lib/templates/rspec/system/system_spec.rb create mode 100644 lib/templates/rspec/view/view_spec.rb diff --git a/lib/templates/rspec/channel/channel_spec.rb.erb b/lib/templates/rspec/channel/channel_spec.rb.erb new file mode 100644 index 00000000..aa067669 --- /dev/null +++ b/lib/templates/rspec/channel/channel_spec.rb.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Channel, <%= type_metatag(:channel) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/templates/rspec/controller/controller_spec.rb b/lib/templates/rspec/controller/controller_spec.rb new file mode 100644 index 00000000..e691da01 --- /dev/null +++ b/lib/templates/rspec/controller/controller_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Controller, <%= type_metatag(:controller) %> do + +<% for action in actions -%> + describe "GET #<%= action %>" do + it "returns http success" do + get :<%= action %> + expect(response).to have_http_status(:success) + end + end + +<% end -%> +end +<% end -%> diff --git a/lib/templates/rspec/controller/request_spec.rb b/lib/templates/rspec/controller/request_spec.rb new file mode 100644 index 00000000..3270392f --- /dev/null +++ b/lib/templates/rspec/controller/request_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= class_name.pluralize %>", <%= type_metatag(:request) %> do +<% namespaced_path = regular_class_path.join('/') %> +<% for action in actions -%> + describe "GET /<%= action %>" do + it "returns http success" do + get "<%= "/#{namespaced_path}" if namespaced_path != '' %>/<%= file_name %>/<%= action %>" + expect(response).to have_http_status(:success) + end + end + +<% end -%> +end diff --git a/lib/templates/rspec/controller/routing_spec.rb b/lib/templates/rspec/controller/routing_spec.rb new file mode 100644 index 00000000..9f211e39 --- /dev/null +++ b/lib/templates/rspec/controller/routing_spec.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe '<%= class_name %>Controller', <%= type_metatag(:routing) %> do + describe 'routing' do +<% for action in actions -%> + it 'routes to #<%= action %>' do + expect(get: "/<%= class_name.underscore %>/<%= action %>").to route_to("<%= class_name.underscore %>#<%= action %>") + end +<% end -%> + end +end +<% end -%> diff --git a/lib/templates/rspec/controller/view_spec.rb b/lib/templates/rspec/controller/view_spec.rb new file mode 100644 index 00000000..531bc44b --- /dev/null +++ b/lib/templates/rspec/controller/view_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= file_name %>/<%= @action %>.html.<%= options[:template_engine] %>", <%= type_metatag(:view) %> do + pending "add some examples to (or delete) #{__FILE__}" +end diff --git a/lib/templates/rspec/feature/feature_singular_spec.rb b/lib/templates/rspec/feature/feature_singular_spec.rb new file mode 100644 index 00000000..67968f78 --- /dev/null +++ b/lib/templates/rspec/feature/feature_singular_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.feature "<%= class_name.singularize %>", <%= type_metatag(:feature) %> do + pending "add some scenarios (or delete) #{__FILE__}" +end diff --git a/lib/templates/rspec/feature/feature_spec.rb b/lib/templates/rspec/feature/feature_spec.rb new file mode 100644 index 00000000..22b6daef --- /dev/null +++ b/lib/templates/rspec/feature/feature_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.feature "<%= class_name.pluralize %>", <%= type_metatag(:feature) %> do + pending "add some scenarios (or delete) #{__FILE__}" +end diff --git a/lib/templates/rspec/generator/generator_spec.rb b/lib/templates/rspec/generator/generator_spec.rb new file mode 100644 index 00000000..1d4a4791 --- /dev/null +++ b/lib/templates/rspec/generator/generator_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= class_name.pluralize %>", <%= type_metatag(:generator) %> do + + pending "add some scenarios (or delete) #{__FILE__}" +end diff --git a/lib/templates/rspec/helper/helper_spec.rb b/lib/templates/rspec/helper/helper_spec.rb new file mode 100644 index 00000000..7c5e51c2 --- /dev/null +++ b/lib/templates/rspec/helper/helper_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +# Specs in this file have access to a helper object that includes +# the <%= class_name %>Helper. For example: +# +# describe <%= class_name %>Helper do +# describe "string concat" do +# it "concats two strings with spaces" do +# expect(helper.concat_strings("this","that")).to eq("this that") +# end +# end +# end +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Helper, <%= type_metatag(:helper) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/templates/rspec/install/spec/rails_helper.rb b/lib/templates/rspec/install/spec/rails_helper.rb new file mode 100644 index 00000000..4a86c981 --- /dev/null +++ b/lib/templates/rspec/install/spec/rails_helper.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# This file is copied to spec/ when you run 'rails generate rspec:install' +require 'spec_helper' +ENV['RAILS_ENV'] ||= 'test' +require File.expand_path('../config/environment', __dir__) +# Prevent database truncation if the environment is production +abort("The Rails environment is running in production mode!") if Rails.env.production? +require 'rspec/rails' +# Add additional requires below this line. Rails is not loaded until this point! + +# Requires supporting ruby files with custom matchers and macros, etc, in +# spec/support/ and its subdirectories. Files matching `spec/**/*_spec.rb` are +# run as spec files by default. This means that files in spec/support that end +# in _spec.rb will both be required and run as specs, causing the specs to be +# run twice. It is recommended that you do not name files matching this glob to +# end with _spec.rb. You can configure this pattern with the --pattern +# option on the command line or in ~/.rspec, .rspec or `.rspec-local`. +# +# The following line is provided for convenience purposes. It has the downside +# of increasing the boot-up time by auto-requiring all files in the support +# directory. Alternatively, in the individual `*_spec.rb` files, manually +# require only the support files necessary. +# +# Dir[Rails.root.join('spec', 'support', '**', '*.rb')].sort.each { |f| require f } + +<% if RSpec::Rails::FeatureCheck.has_active_record_migration? -%> +# Checks for pending migrations and applies them before tests are run. +# If you are not using ActiveRecord, you can remove these lines. +begin + ActiveRecord::Migration.maintain_test_schema! +rescue ActiveRecord::PendingMigrationError => e + puts e.to_s.strip + exit 1 +end +<% end -%> +RSpec.configure do |config| +<% if RSpec::Rails::FeatureCheck.has_active_record? -%> + # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures + config.fixture_path = "#{::Rails.root}/spec/fixtures" + + # If you're not using ActiveRecord, or you'd prefer not to run each of your + # examples within a transaction, remove the following line or assign false + # instead of true. + config.use_transactional_fixtures = true + + # You can uncomment this line to turn off ActiveRecord support entirely. + # config.use_active_record = false + +<% else -%> + # Remove this line to enable support for ActiveRecord + config.use_active_record = false + + # If you enable ActiveRecord support you should unncomment these lines, + # note if you'd prefer not to run each example within a transaction, you + # should set use_transactional_fixtures to false. + # + # config.fixture_path = "#{::Rails.root}/spec/fixtures" + # config.use_transactional_fixtures = true + +<% end -%> + # RSpec Rails can automatically mix in different behaviours to your tests + # based on their file location, for example enabling you to call `get` and + # `post` in specs under `spec/controllers`. + # + # You can disable this behaviour by removing the line below, and instead + # explicitly tag your specs with their type, e.g.: + # + # RSpec.describe UsersController, type: :controller do + # # ... + # end + # + # The different available types are documented in the features, such as in + # https://relishapp.com/rspec/rspec-rails/docs + config.infer_spec_type_from_file_location! + + # Filter lines from Rails gems in backtraces. + config.filter_rails_from_backtrace! + # arbitrary gems may also be filtered via: + # config.filter_gems_from_backtrace("gem name") +end diff --git a/lib/templates/rspec/integration/request_spec.rb b/lib/templates/rspec/integration/request_spec.rb new file mode 100644 index 00000000..2106d5a1 --- /dev/null +++ b/lib/templates/rspec/integration/request_spec.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= class_name.pluralize %>", <%= type_metatag(:request) %> do + describe "GET /<%= name.underscore.pluralize %>" do + it "works! (now write some real specs)" do + get <%= index_helper %>_path + expect(response).to have_http_status(200) + end + end +end diff --git a/lib/templates/rspec/job/job_spec.rb.erb b/lib/templates/rspec/job/job_spec.rb.erb new file mode 100644 index 00000000..dbcb7e55 --- /dev/null +++ b/lib/templates/rspec/job/job_spec.rb.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Job, <%= type_metatag(:job) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/templates/rspec/mailbox/mailbox_spec.rb.erb b/lib/templates/rspec/mailbox/mailbox_spec.rb.erb new file mode 100644 index 00000000..7280025c --- /dev/null +++ b/lib/templates/rspec/mailbox/mailbox_spec.rb.erb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>Mailbox, <%= type_metatag(:mailbox) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/templates/rspec/mailer/fixture b/lib/templates/rspec/mailer/fixture new file mode 100644 index 00000000..171648d6 --- /dev/null +++ b/lib/templates/rspec/mailer/fixture @@ -0,0 +1,3 @@ +<%= class_name %>#<%= @action %> + +Hi, find me in app/views/<%= @path %> diff --git a/lib/templates/rspec/mailer/mailer_spec.rb b/lib/templates/rspec/mailer/mailer_spec.rb new file mode 100644 index 00000000..c72a60ec --- /dev/null +++ b/lib/templates/rspec/mailer/mailer_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require "rails_helper" + +<% module_namespacing do -%> +RSpec.describe <%= Rails.version.to_f >= 5.0 ? class_name.sub(/(Mailer)?$/, 'Mailer') : class_name %>, <%= type_metatag(:mailer) %> do +<% for action in actions -%> + describe "<%= action %>" do + let(:mail) { <%= Rails.version.to_f >= 5.0 ? class_name.sub(/(Mailer)?$/, 'Mailer') : class_name %>.<%= action %> } + + it "renders the headers" do + expect(mail.subject).to eq(<%= action.to_s.humanize.inspect %>) + expect(mail.to).to eq(["to@example.org"]) + expect(mail.from).to eq(["from@example.com"]) + end + + it "renders the body" do + expect(mail.body.encoded).to match("Hi") + end + end + +<% end -%> +<% if actions.blank? -%> + pending "add some examples to (or delete) #{__FILE__}" +<% end -%> +end +<% end -%> diff --git a/lib/templates/rspec/mailer/preview.rb b/lib/templates/rspec/mailer/preview.rb new file mode 100644 index 00000000..d8bfa892 --- /dev/null +++ b/lib/templates/rspec/mailer/preview.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +<% module_namespacing do -%> +# Preview all emails at http://localhost:3000/rails/mailers/<%= file_path %> +class <%= class_name %>Preview < ActionMailer::Preview +<% actions.each do |action| -%> + + # Preview this email at http://localhost:3000/rails/mailers/<%= file_path %>/<%= action %> + def <%= action %> + <%= Rails.version.to_f >= 5.0 ? class_name.sub(/(Mailer)?$/, 'Mailer') : class_name %>.<%= action %> + end +<% end -%> + +end +<% end -%> diff --git a/lib/templates/rspec/model/fixtures.yml b/lib/templates/rspec/model/fixtures.yml new file mode 100644 index 00000000..4a8ab4b4 --- /dev/null +++ b/lib/templates/rspec/model/fixtures.yml @@ -0,0 +1,19 @@ +# Read about fixtures at https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html + +<% unless attributes.empty? -%> +one: +<% for attribute in attributes -%> + <%= attribute.name %>: <%= attribute.default %> +<% end -%> + +two: +<% for attribute in attributes -%> + <%= attribute.name %>: <%= attribute.default %> +<% end -%> +<% else -%> +# one: +# column: value +# +# two: +# column: value +<% end -%> diff --git a/lib/templates/rspec/model/model_spec.rb b/lib/templates/rspec/model/model_spec.rb new file mode 100644 index 00000000..5a4acc49 --- /dev/null +++ b/lib/templates/rspec/model/model_spec.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% module_namespacing do -%> +RSpec.describe <%= class_name %>, <%= type_metatag(:model) %> do + pending "add some examples to (or delete) #{__FILE__}" +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/api_controller_spec.rb b/lib/templates/rspec/scaffold/api_controller_spec.rb new file mode 100644 index 00000000..62b5b0b3 --- /dev/null +++ b/lib/templates/rspec/scaffold/api_controller_spec.rb @@ -0,0 +1,132 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to specify the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. +# +# Compared to earlier versions of this generator, there is very limited use of +# stubs and message expectations in this spec. Stubs are only used when there +# is no simpler way to get a handle on the object needed for the example. +# Message expectations are only used when there is no simpler way to specify +# that an instance is receiving a specific message. +# +# Also compared to earlier versions of this generator, there are no longer any +# expectations of assigns and templates rendered. These features have been +# removed from Rails core in Rails 5, but can be added back in via the +# `rails-controller-testing` gem. + +<% module_namespacing do -%> +RSpec.describe <%= controller_class_name %>Controller, <%= type_metatag(:controller) %> do + + # This should return the minimal set of attributes required to create a valid + # <%= class_name %>. As you add validations to <%= class_name %>, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # <%= controller_class_name %>Controller. Be sure to keep this updated too. + let(:valid_session) { {} } + +<% unless options[:singleton] -%> + describe "GET #index" do + it "returns a success response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + get :index, params: {}, session: valid_session + expect(response).to be_successful + end + end + +<% end -%> + describe "GET #show" do + it "returns a success response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + get :show, params: {id: <%= file_name %>.to_param}, session: valid_session + expect(response).to be_successful + end + end + + describe "POST #create" do + context "with valid params" do + it "creates a new <%= class_name %>" do + expect { + post :create, params: {<%= ns_file_name %>: valid_attributes}, session: valid_session + }.to change(<%= class_name %>, :count).by(1) + end + + it "renders a JSON response with the new <%= ns_file_name %>" do + post :create, params: {<%= ns_file_name %>: valid_attributes}, session: valid_session + expect(response).to have_http_status(:created) + expect(response.content_type).to eq('application/json') + expect(response.location).to eq(<%= ns_file_name %>_url(<%= class_name %>.last)) + end + end + + context "with invalid params" do + it "renders a JSON response with errors for the new <%= ns_file_name %>" do + post :create, params: {<%= ns_file_name %>: invalid_attributes}, session: valid_session + expect(response).to have_http_status(:unprocessable_entity) + expect(response.content_type).to eq('application/json') + end + end + end + + describe "PUT #update" do + context "with valid params" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: new_attributes}, session: valid_session + <%= file_name %>.reload + skip("Add assertions for updated state") + end + + it "renders a JSON response with the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: valid_attributes}, session: valid_session + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq('application/json') + end + end + + context "with invalid params" do + it "renders a JSON response with errors for the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: invalid_attributes}, session: valid_session + expect(response).to have_http_status(:unprocessable_entity) + expect(response.content_type).to eq('application/json') + end + end + end + + describe "DELETE #destroy" do + it "destroys the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + expect { + delete :destroy, params: {id: <%= file_name %>.to_param}, session: valid_session + }.to change(<%= class_name %>, :count).by(-1) + end + end + +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/api_request_spec.rb b/lib/templates/rspec/scaffold/api_request_spec.rb new file mode 100644 index 00000000..0fec9c9b --- /dev/null +++ b/lib/templates/rspec/scaffold/api_request_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +<% module_namespacing do -%> +RSpec.describe "/<%= name.underscore.pluralize %>", <%= type_metatag(:request) %> do + # This should return the minimal set of attributes required to create a valid + # <%= class_name %>. As you add validations to <%= class_name %>, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + # This should return the minimal set of values that should be in the headers + # in order to pass any filters (e.g. authentication) defined in + # <%= controller_class_name %>Controller, or in your router and rack + # middleware. Be sure to keep this updated too. + let(:valid_headers) { + {} + } + +<% unless options[:singleton] -%> + describe "GET /index" do + it "renders a successful response" do + <%= class_name %>.create! valid_attributes + get <%= index_helper %>_url, headers: valid_headers, as: :json + expect(response).to be_successful + end + end +<% end -%> + + describe "GET /show" do + it "renders a successful response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + get <%= show_helper.tr('@', '') %>, as: :json + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new <%= class_name %>" do + expect { + post <%= index_helper %>_url, + params: { <%= ns_file_name %>: valid_attributes }, headers: valid_headers, as: :json + }.to change(<%= class_name %>, :count).by(1) + end + + it "renders a JSON response with the new <%= ns_file_name %>" do + post <%= index_helper %>_url, + params: { <%= ns_file_name %>: valid_attributes }, headers: valid_headers, as: :json + expect(response).to have_http_status(:created) + expect(response.content_type).to match(a_string_including("application/json")) + end + end + + context "with invalid parameters" do + it "does not create a new <%= class_name %>" do + expect { + post <%= index_helper %>_url, + params: { <%= ns_file_name %>: invalid_attributes }, as: :json + }.to change(<%= class_name %>, :count).by(0) + end + + it "renders a JSON response with errors for the new <%= ns_file_name %>" do + post <%= index_helper %>_url, + params: { <%= ns_file_name %>: invalid_attributes }, headers: valid_headers, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(response.content_type).to eq("application/json") + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, + params: { <%= singular_table_name %>: invalid_attributes }, headers: valid_headers, as: :json + <%= file_name %>.reload + skip("Add assertions for updated state") + end + + it "renders a JSON response with the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, + params: { <%= singular_table_name %>: invalid_attributes }, headers: valid_headers, as: :json + expect(response).to have_http_status(:ok) + expect(response.content_type).to eq("application/json") + end + end + + context "with invalid parameters" do + it "renders a JSON response with errors for the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, + params: { <%= singular_table_name %>: invalid_attributes }, headers: valid_headers, as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(response.content_type).to eq("application/json") + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + expect { + delete <%= show_helper.tr('@', '') %>, headers: valid_headers, as: :json + }.to change(<%= class_name %>, :count).by(-1) + end + end +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/controller_spec.rb b/lib/templates/rspec/scaffold/controller_spec.rb new file mode 100644 index 00000000..f6d16a9b --- /dev/null +++ b/lib/templates/rspec/scaffold/controller_spec.rb @@ -0,0 +1,196 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to specify the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. +# +# Compared to earlier versions of this generator, there is very limited use of +# stubs and message expectations in this spec. Stubs are only used when there +# is no simpler way to get a handle on the object needed for the example. +# Message expectations are only used when there is no simpler way to specify +# that an instance is receiving a specific message. +# +# Also compared to earlier versions of this generator, there are no longer any +# expectations of assigns and templates rendered. These features have been +# removed from Rails core in Rails 5, but can be added back in via the +# `rails-controller-testing` gem. + +<% module_namespacing do -%> +RSpec.describe <%= controller_class_name %>Controller, <%= type_metatag(:controller) %> do + + # This should return the minimal set of attributes required to create a valid + # <%= class_name %>. As you add validations to <%= class_name %>, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + + # This should return the minimal set of values that should be in the session + # in order to pass any filters (e.g. authentication) defined in + # <%= controller_class_name %>Controller. Be sure to keep this updated too. + let(:valid_session) { {} } + +<% unless options[:singleton] -%> + describe "GET #index" do + it "returns a success response" do + <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + get :index, {}, valid_session +<% else -%> + get :index, params: {}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + +<% end -%> + describe "GET #show" do + it "returns a success response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + get :show, {id: <%= file_name %>.to_param}, valid_session +<% else -%> + get :show, params: {id: <%= file_name %>.to_param}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + + describe "GET #new" do + it "returns a success response" do +<% if Rails::VERSION::STRING < '5.0' -%> + get :new, {}, valid_session +<% else -%> + get :new, params: {}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + + describe "GET #edit" do + it "returns a success response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + get :edit, {id: <%= file_name %>.to_param}, valid_session +<% else -%> + get :edit, params: {id: <%= file_name %>.to_param}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + + describe "POST #create" do + context "with valid params" do + it "creates a new <%= class_name %>" do + expect { +<% if Rails::VERSION::STRING < '5.0' -%> + post :create, {<%= ns_file_name %>: valid_attributes}, valid_session +<% else -%> + post :create, params: {<%= ns_file_name %>: valid_attributes}, session: valid_session +<% end -%> + }.to change(<%= class_name %>, :count).by(1) + end + + it "redirects to the created <%= ns_file_name %>" do +<% if Rails::VERSION::STRING < '5.0' -%> + post :create, {<%= ns_file_name %>: valid_attributes}, valid_session +<% else -%> + post :create, params: {<%= ns_file_name %>: valid_attributes}, session: valid_session +<% end -%> + expect(response).to redirect_to(<%= class_name %>.last) + end + end + + context "with invalid params" do + it "returns a success response (i.e. to display the 'new' template)" do +<% if Rails::VERSION::STRING < '5.0' -%> + post :create, {<%= ns_file_name %>: invalid_attributes}, valid_session +<% else -%> + post :create, params: {<%= ns_file_name %>: invalid_attributes}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + end + + describe "PUT #update" do + context "with valid params" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + put :update, {id: <%= file_name %>.to_param, <%= ns_file_name %>: new_attributes}, valid_session +<% else -%> + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: new_attributes}, session: valid_session +<% end -%> + <%= file_name %>.reload + skip("Add assertions for updated state") + end + + it "redirects to the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + put :update, {id: <%= file_name %>.to_param, <%= ns_file_name %>: valid_attributes}, valid_session +<% else -%> + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: valid_attributes}, session: valid_session +<% end -%> + expect(response).to redirect_to(<%= file_name %>) + end + end + + context "with invalid params" do + it "returns a success response (i.e. to display the 'edit' template)" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + put :update, {id: <%= file_name %>.to_param, <%= ns_file_name %>: invalid_attributes}, valid_session +<% else -%> + put :update, params: {id: <%= file_name %>.to_param, <%= ns_file_name %>: invalid_attributes}, session: valid_session +<% end -%> + expect(response).to be_successful + end + end + end + + describe "DELETE #destroy" do + it "destroys the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + expect { +<% if Rails::VERSION::STRING < '5.0' -%> + delete :destroy, {id: <%= file_name %>.to_param}, valid_session +<% else -%> + delete :destroy, params: {id: <%= file_name %>.to_param}, session: valid_session +<% end -%> + }.to change(<%= class_name %>, :count).by(-1) + end + + it "redirects to the <%= table_name %> list" do + <%= file_name %> = <%= class_name %>.create! valid_attributes +<% if Rails::VERSION::STRING < '5.0' -%> + delete :destroy, {id: <%= file_name %>.to_param}, valid_session +<% else -%> + delete :destroy, params: {id: <%= file_name %>.to_param}, session: valid_session +<% end -%> + expect(response).to redirect_to(<%= index_helper %>_url) + end + end + +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/edit_spec.rb b/lib/templates/rspec/scaffold/edit_spec.rb new file mode 100644 index 00000000..a7ea64ed --- /dev/null +++ b/lib/templates/rspec/scaffold/edit_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% output_attributes = attributes.reject{|attribute| [:datetime, :timestamp, :time, :date].index(attribute.type) } -%> +RSpec.describe "<%= ns_table_name %>/edit", <%= type_metatag(:view) %> do + before(:each) do + @<%= ns_file_name %> = assign(:<%= ns_file_name %>, <%= class_name %>.create!(<%= '))' if output_attributes.empty? %> +<% output_attributes.each_with_index do |attribute, attribute_index| -%> + <%= attribute.name %>: <%= attribute.default.inspect %><%= attribute_index == output_attributes.length - 1 ? '' : ','%> +<% end -%> +<%= output_attributes.empty? ? "" : " ))\n" -%> + end + + it "renders the edit <%= ns_file_name %> form" do + render + + assert_select "form[action=?][method=?]", <%= ns_file_name %>_path(@<%= ns_file_name %>), "post" do +<% for attribute in output_attributes -%> + <%- name = attribute.respond_to?(:column_name) ? attribute.column_name : attribute.name %> +<% if Rails.version.to_f >= 5.1 -%> + assert_select "<%= attribute.input_type -%>[name=?]", "<%= ns_file_name %>[<%= name %>]" +<% else -%> + assert_select "<%= attribute.input_type -%>#<%= ns_file_name %>_<%= name %>[name=?]", "<%= ns_file_name %>[<%= name %>]" +<% end -%> +<% end -%> + end + end +end diff --git a/lib/templates/rspec/scaffold/index_spec.rb b/lib/templates/rspec/scaffold/index_spec.rb new file mode 100644 index 00000000..cb703f1b --- /dev/null +++ b/lib/templates/rspec/scaffold/index_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% output_attributes = attributes.reject{|attribute| [:datetime, :timestamp, :time, :date].index(attribute.type) } -%> +RSpec.describe "<%= ns_table_name %>/index", <%= type_metatag(:view) %> do + before(:each) do + assign(:<%= table_name %>, [ +<% [1,2].each_with_index do |id, model_index| -%> + <%= class_name %>.create!(<%= output_attributes.empty? ? (model_index == 1 ? ')' : '),') : '' %> +<% output_attributes.each_with_index do |attribute, attribute_index| -%> + <%= attribute.name %>: <%= value_for(attribute) %><%= attribute_index == output_attributes.length - 1 ? '' : ','%> +<% end -%> +<% if !output_attributes.empty? -%> + <%= model_index == 1 ? ')' : '),' %> +<% end -%> +<% end -%> + ]) + end + + it "renders a list of <%= ns_table_name %>" do + render +<% for attribute in output_attributes -%> + assert_select "tr>td", text: <%= value_for(attribute) %>.to_s, count: 2 +<% end -%> + end +end diff --git a/lib/templates/rspec/scaffold/new_spec.rb b/lib/templates/rspec/scaffold/new_spec.rb new file mode 100644 index 00000000..befa8d4d --- /dev/null +++ b/lib/templates/rspec/scaffold/new_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% output_attributes = attributes.reject{|attribute| [:datetime, :timestamp, :time, :date].index(attribute.type) } -%> +RSpec.describe "<%= ns_table_name %>/new", <%= type_metatag(:view) %> do + before(:each) do + assign(:<%= ns_file_name %>, <%= class_name %>.new(<%= '))' if output_attributes.empty? %> +<% output_attributes.each_with_index do |attribute, attribute_index| -%> + <%= attribute.name %>: <%= attribute.default.inspect %><%= attribute_index == output_attributes.length - 1 ? '' : ','%> +<% end -%> +<%= !output_attributes.empty? ? " ))\n end" : " end" %> + + it "renders new <%= ns_file_name %> form" do + render + + assert_select "form[action=?][method=?]", <%= index_helper %>_path, "post" do +<% for attribute in output_attributes -%> + <%- name = attribute.respond_to?(:column_name) ? attribute.column_name : attribute.name %> +<% if Rails.version.to_f >= 5.1 -%> + assert_select "<%= attribute.input_type -%>[name=?]", "<%= ns_file_name %>[<%= name %>]" +<% else -%> + assert_select "<%= attribute.input_type -%>#<%= ns_file_name %>_<%= name %>[name=?]", "<%= ns_file_name %>[<%= name %>]" +<% end -%> +<% end -%> + end + end +end diff --git a/lib/templates/rspec/scaffold/request_spec.rb b/lib/templates/rspec/scaffold/request_spec.rb new file mode 100644 index 00000000..b575879f --- /dev/null +++ b/lib/templates/rspec/scaffold/request_spec.rb @@ -0,0 +1,136 @@ + # frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + require 'rails_helper' + +# This spec was generated by rspec-rails when you ran the scaffold generator. +# It demonstrates how one might use RSpec to test the controller code that +# was generated by Rails when you ran the scaffold generator. +# +# It assumes that the implementation code is generated by the rails scaffold +# generator. If you are using any extension libraries to generate different +# controller code, this generated spec may or may not pass. +# +# It only uses APIs available in rails and/or rspec-rails. There are a number +# of tools you can use to make these specs even more expressive, but we're +# sticking to rails and rspec-rails APIs to keep things simple and stable. + +<% module_namespacing do -%> +RSpec.describe "/<%= name.underscore.pluralize %>", <%= type_metatag(:request) %> do + # <%= class_name %>. As you add validations to <%= class_name %>, be sure to + # adjust the attributes here as well. + let(:valid_attributes) { + skip("Add a hash of attributes valid for your model") + } + + let(:invalid_attributes) { + skip("Add a hash of attributes invalid for your model") + } + +<% unless options[:singleton] -%> + describe "GET /index" do + it "renders a successful response" do + <%= class_name %>.create! valid_attributes + get <%= index_helper %>_url + expect(response).to be_successful + end + end +<% end -%> + + describe "GET /show" do + it "renders a successful response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + get <%= show_helper.tr('@', '') %> + expect(response).to be_successful + end + end + + describe "GET /new" do + it "renders a successful response" do + get <%= new_helper %> + expect(response).to be_successful + end + end + + describe "GET /edit" do + it "render a successful response" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + get <%= edit_helper.tr('@','') %> + expect(response).to be_successful + end + end + + describe "POST /create" do + context "with valid parameters" do + it "creates a new <%= class_name %>" do + expect { + post <%= index_helper %>_url, params: { <%= ns_file_name %>: valid_attributes } + }.to change(<%= class_name %>, :count).by(1) + end + + it "redirects to the created <%= ns_file_name %>" do + post <%= index_helper %>_url, params: { <%= ns_file_name %>: valid_attributes } + expect(response).to redirect_to(<%= show_helper.gsub("\@#{file_name}", class_name+".last") %>) + end + end + + context "with invalid parameters" do + it "does not create a new <%= class_name %>" do + expect { + post <%= index_helper %>_url, params: { <%= ns_file_name %>: invalid_attributes } + }.to change(<%= class_name %>, :count).by(0) + end + + it "renders a successful response (i.e. to display the 'new' template)" do + post <%= index_helper %>_url, params: { <%= ns_file_name %>: invalid_attributes } + expect(response).to be_successful + end + end + end + + describe "PATCH /update" do + context "with valid parameters" do + let(:new_attributes) { + skip("Add a hash of attributes valid for your model") + } + + it "updates the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, params: { <%= singular_table_name %>: new_attributes } + <%= file_name %>.reload + skip("Add assertions for updated state") + end + + it "redirects to the <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, params: { <%= singular_table_name %>: new_attributes } + <%= file_name %>.reload + expect(response).to redirect_to(<%= singular_table_name %>_url(<%= file_name %>)) + end + end + + context "with invalid parameters" do + it "renders a successful response (i.e. to display the 'edit' template)" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + patch <%= show_helper.tr('@', '') %>, params: { <%= singular_table_name %>: invalid_attributes } + expect(response).to be_successful + end + end + end + + describe "DELETE /destroy" do + it "destroys the requested <%= ns_file_name %>" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + expect { + delete <%= show_helper.tr('@', '') %> + }.to change(<%= class_name %>, :count).by(-1) + end + + it "redirects to the <%= table_name %> list" do + <%= file_name %> = <%= class_name %>.create! valid_attributes + delete <%= show_helper.tr('@', '') %> + expect(response).to redirect_to(<%= index_helper %>_url) + end + end +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/routing_spec.rb b/lib/templates/rspec/scaffold/routing_spec.rb new file mode 100644 index 00000000..7b35a6a8 --- /dev/null +++ b/lib/templates/rspec/scaffold/routing_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require "rails_helper" + +<% module_namespacing do -%> +RSpec.describe <%= controller_class_name %>Controller, <%= type_metatag(:routing) %> do + describe "routing" do +<% unless options[:singleton] -%> + it "routes to #index" do + expect(get: "/<%= ns_table_name %>").to route_to("<%= ns_table_name %>#index") + end + +<% end -%> +<% unless options[:api] -%> + it "routes to #new" do + expect(get: "/<%= ns_table_name %>/new").to route_to("<%= ns_table_name %>#new") + end + +<% end -%> + it "routes to #show" do + expect(get: "/<%= ns_table_name %>/1").to route_to("<%= ns_table_name %>#show", id: "1") + end + +<% unless options[:api] -%> + it "routes to #edit" do + expect(get: "/<%= ns_table_name %>/1/edit").to route_to("<%= ns_table_name %>#edit", id: "1") + end + +<% end -%> + + it "routes to #create" do + expect(post: "/<%= ns_table_name %>").to route_to("<%= ns_table_name %>#create") + end + + it "routes to #update via PUT" do + expect(put: "/<%= ns_table_name %>/1").to route_to("<%= ns_table_name %>#update", id: "1") + end + + it "routes to #update via PATCH" do + expect(patch: "/<%= ns_table_name %>/1").to route_to("<%= ns_table_name %>#update", id: "1") + end + + it "routes to #destroy" do + expect(delete: "/<%= ns_table_name %>/1").to route_to("<%= ns_table_name %>#destroy", id: "1") + end + end +end +<% end -%> diff --git a/lib/templates/rspec/scaffold/show_spec.rb b/lib/templates/rspec/scaffold/show_spec.rb new file mode 100644 index 00000000..c6f13948 --- /dev/null +++ b/lib/templates/rspec/scaffold/show_spec.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +<% output_attributes = attributes.reject{|attribute| [:datetime, :timestamp, :time, :date].index(attribute.type) } -%> +RSpec.describe "<%= ns_table_name %>/show", <%= type_metatag(:view) %> do + before(:each) do + @<%= ns_file_name %> = assign(:<%= ns_file_name %>, <%= class_name %>.create!(<%= '))' if output_attributes.empty? %> +<% output_attributes.each_with_index do |attribute, attribute_index| -%> + <%= attribute.name %>: <%= value_for(attribute) %><%= attribute_index == output_attributes.length - 1 ? '' : ','%> +<% end -%> +<% if !output_attributes.empty? -%> + )) +<% end -%> + end + + it "renders attributes in

" do + render +<% for attribute in output_attributes -%> + expect(rendered).to match(/<%= raw_value_for(attribute) %>/) +<% end -%> + end +end diff --git a/lib/templates/rspec/system/system_spec.rb b/lib/templates/rspec/system/system_spec.rb new file mode 100644 index 00000000..3e606d7e --- /dev/null +++ b/lib/templates/rspec/system/system_spec.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= class_name.pluralize %>", <%= type_metatag(:system) %> do + before do + driven_by(:rack_test) + end + + pending "add some scenarios (or delete) #{__FILE__}" +end diff --git a/lib/templates/rspec/view/view_spec.rb b/lib/templates/rspec/view/view_spec.rb new file mode 100644 index 00000000..580f5e8d --- /dev/null +++ b/lib/templates/rspec/view/view_spec.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'rails_helper' + +RSpec.describe "<%= file_path %>/<%= @action %>", <%= type_metatag(:view) %> do + pending "add some examples to (or delete) #{__FILE__}" +end From 3c55acaa3d585122564dd5a08ae150989470be1d Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 14:34:07 -0500 Subject: [PATCH 312/440] Add factory_bot templates --- lib/templates/factory_bot/model/factories.erb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 lib/templates/factory_bot/model/factories.erb diff --git a/lib/templates/factory_bot/model/factories.erb b/lib/templates/factory_bot/model/factories.erb new file mode 100644 index 00000000..00ab5121 --- /dev/null +++ b/lib/templates/factory_bot/model/factories.erb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +FactoryBot.define do +<%= factory_definition.rstrip %> +end From bcde594b31556e8d7e5e3dbbc65371aa46a612ab Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 15:28:24 -0500 Subject: [PATCH 313/440] Created houdini_upgrade gem by running exist identical README.md identical Rakefile identical houdini_upgrade.gemspec identical LICENSE identical AGPL-3.0.txt identical LGPL-3.0.txt identical GPL-3.0.txt exist app identical app/controllers/houdini_upgrade/application_controller.rb identical app/helpers/houdini_upgrade/application_helper.rb identical app/jobs/houdini_upgrade/application_job.rb identical app/mailers/houdini_upgrade/application_mailer.rb identical app/models/houdini_upgrade/application_record.rb identical app/views/layouts/houdini_upgrade/application.html.erb exist app/assets/images/houdini_upgrade identical config/routes.rb identical lib/houdini_upgrade.rb identical lib/tasks/houdini_upgrade_tasks.rake identical lib/houdini_upgrade/version.rb identical lib/houdini_upgrade/engine.rb identical app/assets/config/houdini_upgrade_manifest.js identical app/assets/stylesheets/houdini_upgrade/application.css identical app/assets/javascripts/houdini_upgrade/application.js identical bin/rails --- gems/houdini_upgrade/AGPL-3.0.txt | 661 +++++++++++++++++ gems/houdini_upgrade/GPL-3.0.txt | 674 ++++++++++++++++++ gems/houdini_upgrade/LGPL-3.0.txt | 165 +++++ gems/houdini_upgrade/LICENSE | 86 +++ gems/houdini_upgrade/README.md | 25 + gems/houdini_upgrade/Rakefile | 8 + .../assets/config/houdini_upgrade_manifest.js | 2 + .../houdini_upgrade/application.js | 15 + .../houdini_upgrade/application.css | 15 + .../houdini_upgrade/application_controller.rb | 8 + .../houdini_upgrade/application_helper.rb | 7 + .../jobs/houdini_upgrade/application_job.rb | 7 + .../houdini_upgrade/application_mailer.rb | 9 + .../houdini_upgrade/application_record.rb | 8 + .../houdini_upgrade/application.html.erb | 16 + gems/houdini_upgrade/bin/rails | 24 + gems/houdini_upgrade/config/routes.rb | 2 + gems/houdini_upgrade/houdini_upgrade.gemspec | 24 + gems/houdini_upgrade/lib/houdini_upgrade.rb | 8 + .../lib/houdini_upgrade/engine.rb | 8 + .../lib/houdini_upgrade/version.rb | 6 + .../lib/tasks/houdini_upgrade_tasks.rake | 7 + 22 files changed, 1785 insertions(+) create mode 100644 gems/houdini_upgrade/AGPL-3.0.txt create mode 100644 gems/houdini_upgrade/GPL-3.0.txt create mode 100644 gems/houdini_upgrade/LGPL-3.0.txt create mode 100644 gems/houdini_upgrade/LICENSE create mode 100644 gems/houdini_upgrade/README.md create mode 100644 gems/houdini_upgrade/Rakefile create mode 100644 gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js create mode 100644 gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js create mode 100644 gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css create mode 100644 gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb create mode 100644 gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb create mode 100644 gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb create mode 100644 gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb create mode 100644 gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb create mode 100644 gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb create mode 100755 gems/houdini_upgrade/bin/rails create mode 100644 gems/houdini_upgrade/config/routes.rb create mode 100644 gems/houdini_upgrade/houdini_upgrade.gemspec create mode 100644 gems/houdini_upgrade/lib/houdini_upgrade.rb create mode 100644 gems/houdini_upgrade/lib/houdini_upgrade/engine.rb create mode 100644 gems/houdini_upgrade/lib/houdini_upgrade/version.rb create mode 100644 gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake diff --git a/gems/houdini_upgrade/AGPL-3.0.txt b/gems/houdini_upgrade/AGPL-3.0.txt new file mode 100644 index 00000000..be3f7b28 --- /dev/null +++ b/gems/houdini_upgrade/AGPL-3.0.txt @@ -0,0 +1,661 @@ + GNU AFFERO GENERAL PUBLIC LICENSE + Version 3, 19 November 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU Affero General Public License is a free, copyleft license for +software and other kinds of works, specifically designed to ensure +cooperation with the community in the case of network server software. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +our General Public Licenses are intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + Developers that use our General Public Licenses protect your rights +with two steps: (1) assert copyright on the software, and (2) offer +you this License which gives you legal permission to copy, distribute +and/or modify the software. + + A secondary benefit of defending all users' freedom is that +improvements made in alternate versions of the program, if they +receive widespread use, become available for other developers to +incorporate. Many developers of free software are heartened and +encouraged by the resulting cooperation. However, in the case of +software used on network servers, this result may fail to come about. +The GNU General Public License permits making a modified version and +letting the public access it on a server without ever releasing its +source code to the public. + + The GNU Affero General Public License is designed specifically to +ensure that, in such cases, the modified source code becomes available +to the community. It requires the operator of a network server to +provide the source code of the modified version running there to the +users of that server. Therefore, public use of a modified version, on +a publicly accessible server, gives the public access to the source +code of the modified version. + + An older license, called the Affero General Public License and +published by Affero, was designed to accomplish similar goals. This is +a different license, not a version of the Affero GPL, but Affero has +released a new version of the Affero GPL which permits relicensing under +this license. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU Affero General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Remote Network Interaction; Use with the GNU General Public License. + + Notwithstanding any other provision of this License, if you modify the +Program, your modified version must prominently offer all users +interacting with it remotely through a computer network (if your version +supports such interaction) an opportunity to receive the Corresponding +Source of your version by providing access to the Corresponding Source +from a network server at no charge, through some standard or customary +means of facilitating copying of software. This Corresponding Source +shall include the Corresponding Source for any work covered by version 3 +of the GNU General Public License that is incorporated pursuant to the +following paragraph. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the work with which it is combined will remain governed by version +3 of the GNU General Public License. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU Affero General Public License from time to time. Such new versions +will be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU Affero General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU Affero General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU Affero General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If your software can interact with users remotely through a computer +network, you should also make sure that it provides a way for users to +get its source. For example, if your program is a web application, its +interface could display a "Source" link that leads users to an archive +of the code. There are many ways you could offer source, and different +solutions will be better for different programs; see section 13 for the +specific requirements. + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU AGPL, see +. diff --git a/gems/houdini_upgrade/GPL-3.0.txt b/gems/houdini_upgrade/GPL-3.0.txt new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/gems/houdini_upgrade/GPL-3.0.txt @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/gems/houdini_upgrade/LGPL-3.0.txt b/gems/houdini_upgrade/LGPL-3.0.txt new file mode 100644 index 00000000..0a041280 --- /dev/null +++ b/gems/houdini_upgrade/LGPL-3.0.txt @@ -0,0 +1,165 @@ + GNU LESSER GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + + This version of the GNU Lesser General Public License incorporates +the terms and conditions of version 3 of the GNU General Public +License, supplemented by the additional permissions listed below. + + 0. Additional Definitions. + + As used herein, "this License" refers to version 3 of the GNU Lesser +General Public License, and the "GNU GPL" refers to version 3 of the GNU +General Public License. + + "The Library" refers to a covered work governed by this License, +other than an Application or a Combined Work as defined below. + + An "Application" is any work that makes use of an interface provided +by the Library, but which is not otherwise based on the Library. +Defining a subclass of a class defined by the Library is deemed a mode +of using an interface provided by the Library. + + A "Combined Work" is a work produced by combining or linking an +Application with the Library. The particular version of the Library +with which the Combined Work was made is also called the "Linked +Version". + + The "Minimal Corresponding Source" for a Combined Work means the +Corresponding Source for the Combined Work, excluding any source code +for portions of the Combined Work that, considered in isolation, are +based on the Application, and not on the Linked Version. + + The "Corresponding Application Code" for a Combined Work means the +object code and/or source code for the Application, including any data +and utility programs needed for reproducing the Combined Work from the +Application, but excluding the System Libraries of the Combined Work. + + 1. Exception to Section 3 of the GNU GPL. + + You may convey a covered work under sections 3 and 4 of this License +without being bound by section 3 of the GNU GPL. + + 2. Conveying Modified Versions. + + If you modify a copy of the Library, and, in your modifications, a +facility refers to a function or data to be supplied by an Application +that uses the facility (other than as an argument passed when the +facility is invoked), then you may convey a copy of the modified +version: + + a) under this License, provided that you make a good faith effort to + ensure that, in the event an Application does not supply the + function or data, the facility still operates, and performs + whatever part of its purpose remains meaningful, or + + b) under the GNU GPL, with none of the additional permissions of + this License applicable to that copy. + + 3. Object Code Incorporating Material from Library Header Files. + + The object code form of an Application may incorporate material from +a header file that is part of the Library. You may convey such object +code under terms of your choice, provided that, if the incorporated +material is not limited to numerical parameters, data structure +layouts and accessors, or small macros, inline functions and templates +(ten or fewer lines in length), you do both of the following: + + a) Give prominent notice with each copy of the object code that the + Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the object code with a copy of the GNU GPL and this license + document. + + 4. Combined Works. + + You may convey a Combined Work under terms of your choice that, +taken together, effectively do not restrict modification of the +portions of the Library contained in the Combined Work and reverse +engineering for debugging such modifications, if you also do each of +the following: + + a) Give prominent notice with each copy of the Combined Work that + the Library is used in it and that the Library and its use are + covered by this License. + + b) Accompany the Combined Work with a copy of the GNU GPL and this license + document. + + c) For a Combined Work that displays copyright notices during + execution, include the copyright notice for the Library among + these notices, as well as a reference directing the user to the + copies of the GNU GPL and this license document. + + d) Do one of the following: + + 0) Convey the Minimal Corresponding Source under the terms of this + License, and the Corresponding Application Code in a form + suitable for, and under terms that permit, the user to + recombine or relink the Application with a modified version of + the Linked Version to produce a modified Combined Work, in the + manner specified by section 6 of the GNU GPL for conveying + Corresponding Source. + + 1) Use a suitable shared library mechanism for linking with the + Library. A suitable mechanism is one that (a) uses at run time + a copy of the Library already present on the user's computer + system, and (b) will operate properly with a modified version + of the Library that is interface-compatible with the Linked + Version. + + e) Provide Installation Information, but only if you would otherwise + be required to provide such information under section 6 of the + GNU GPL, and only to the extent that such information is + necessary to install and execute a modified version of the + Combined Work produced by recombining or relinking the + Application with a modified version of the Linked Version. (If + you use option 4d0, the Installation Information must accompany + the Minimal Corresponding Source and Corresponding Application + Code. If you use option 4d1, you must provide the Installation + Information in the manner specified by section 6 of the GNU GPL + for conveying Corresponding Source.) + + 5. Combined Libraries. + + You may place library facilities that are a work based on the +Library side by side in a single library together with other library +facilities that are not Applications and are not covered by this +License, and convey such a combined library under terms of your +choice, if you do both of the following: + + a) Accompany the combined library with a copy of the same work based + on the Library, uncombined with any other library facilities, + conveyed under the terms of this License. + + b) Give prominent notice with the combined library that part of it + is a work based on the Library, and explaining where to find the + accompanying uncombined form of the same work. + + 6. Revised Versions of the GNU Lesser General Public License. + + The Free Software Foundation may publish revised and/or new versions +of the GNU Lesser General Public License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. + + Each version is given a distinguishing version number. If the +Library as you received it specifies that a certain numbered version +of the GNU Lesser General Public License "or any later version" +applies to it, you have the option of following the terms and +conditions either of that published version or of any later version +published by the Free Software Foundation. If the Library as you +received it does not specify a version number of the GNU Lesser +General Public License, you may choose any version of the GNU Lesser +General Public License ever published by the Free Software Foundation. + + If the Library as you received it specifies that a proxy can decide +whether future versions of the GNU Lesser General Public License shall +apply, that proxy's public statement of acceptance of any version is +permanent authorization for you to choose that version for the +Library. diff --git a/gems/houdini_upgrade/LICENSE b/gems/houdini_upgrade/LICENSE new file mode 100644 index 00000000..205ae966 --- /dev/null +++ b/gems/houdini_upgrade/LICENSE @@ -0,0 +1,86 @@ +# Free Software Licensing Information for "houdini_upgrade". + +The primary license of "houdini_upgrade" is: + + AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +This software's license gives you freedom; you can copy, convey, +propagate, redistribute and/or modify this program under the terms of +the GNU Affero General Public License (AGPL) as published by the Free +Software Foundation (FSF), either version 3 of the License, or (at your +option) any later version of the AGPL published by the FSF. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero +General Public License for more details. + +You should have received a copy of the GNU Affero General Public License +along with this program in a file in the toplevel directory called +"AGPL-3.0.txt". If not, see . + +In addition to the permission granted by the AGPLv3, you also receive +permissions as written in The Web Template Output Additional Permission, +Version 3.0, as published by the Software Freedom Conservancy +("Conservancy"), either version 3 of that Additional Permission, or (at your +option) any later version of the Additional Permission as published by +Conservancy. + +You should have received a copy of the Web Template Output Additional +Permission, along with this program in a file in the toplevel directory +called "Web-Template-Output-Additional-Permission.txt". If not, see +. + +## Different Licenses for some files in "houdini_upgrade" Repository. + +1. Any file in the "houdini_upgrade" repository that does not specific a license, or is + not discussed explicitly in this toplevel "LICENSE" file is licensed: + + AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + + as specified in detail above. + +2. Files that contain the information "License: LGPL-3.0-or-later" are + licensed as below: + + You can copy, convey, propagate, redistribute and/or modify this program + under the terms of the GNU General Public License (GPL) as published by + the Free Software Foundation (FSF), either version 3 of the License, or + (at your option) any later version of the GPL published by the FSF. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + License for more details. + + You should have received a copy of the GNU General Public License along + with this program in a file in the toplevel directory called + "GPL-3.0.txt". If not, see . + + In addition to the permission granted by the GPLv3, you also receive + permissions as written in GNU Lesser General Public License (LGPL), + Version 3.0, as published by the Free Software Foundation (FSF), either + version 3 of the License, or (at your option) any later version of the + LGPL published by the FSF. + + This program is distributed in the hope that it will be useful, but + WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY + or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program in a file in the toplevel directory called + "LGPL-3.0.txt". If not, see . + +3. Files that contain the information "License: CC0-1.0" are licensed under + the Creative Commons Public Domain Dedication 1.0 Universal or any later + version as published by Creative Commons, Inc. ("CC0"). + +4. All SVG, ICO and PNG files in this repository are licensed as CC-0. + + This applies to any files that are both (a) in the standard SVG, ICO, + and/or PNG format, and (b) where their names end in .svg or .ico. + + The project may in future place later versions of these files under a + copyleft license, but will discuss that with the contributor community + before doing so. diff --git a/gems/houdini_upgrade/README.md b/gems/houdini_upgrade/README.md new file mode 100644 index 00000000..4d0b0826 --- /dev/null +++ b/gems/houdini_upgrade/README.md @@ -0,0 +1,25 @@ +# HoudiniUpgrade +Short description and motivation. + +## Usage +How to use my plugin. + +## Installation +Add this line to your application's Gemfile: + +```ruby +gem 'houdini_upgrade' +``` + +And then execute: +```bash +$ bundle +``` + +Or install it yourself as: +```bash +$ gem install houdini_upgrade +``` + +## Contributing +Contribution directions go here. diff --git a/gems/houdini_upgrade/Rakefile b/gems/houdini_upgrade/Rakefile new file mode 100644 index 00000000..3039823b --- /dev/null +++ b/gems/houdini_upgrade/Rakefile @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require "bundler/setup" + +load "rails/tasks/statistics.rake" + +require "bundler/gem_tasks" diff --git a/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js b/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js new file mode 100644 index 00000000..6c65c4eb --- /dev/null +++ b/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js @@ -0,0 +1,2 @@ +//= link_directory ../javascripts/houdini_upgrade .js +//= link_directory ../stylesheets/houdini_upgrade .css diff --git a/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js b/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js new file mode 100644 index 00000000..67ce4675 --- /dev/null +++ b/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js @@ -0,0 +1,15 @@ +// This is a manifest file that'll be compiled into application.js, which will include all the files +// listed below. +// +// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, +// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. +// +// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the +// compiled file. JavaScript code in this file should be added after the last require_* statement. +// +// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details +// about supported directives. +// +//= require rails-ujs +//= require activestorage +//= require_tree . diff --git a/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css b/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css new file mode 100644 index 00000000..0ebd7fe8 --- /dev/null +++ b/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css @@ -0,0 +1,15 @@ +/* + * This is a manifest file that'll be compiled into application.css, which will include all the files + * listed below. + * + * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, + * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. + * + * You're free to add application-wide styles to this file and they'll appear at the bottom of the + * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS + * files in this directory. Styles in this file should be added after the last require_* statement. + * It is generally better to create a new file per style scope. + * + *= require_tree . + *= require_self + */ diff --git a/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb b/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb new file mode 100644 index 00000000..27abf6e8 --- /dev/null +++ b/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + class ApplicationController < ActionController::Base + protect_from_forgery with: :exception + end +end diff --git a/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb b/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb new file mode 100644 index 00000000..392c3d58 --- /dev/null +++ b/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + module ApplicationHelper + end +end diff --git a/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb b/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb new file mode 100644 index 00000000..2c297597 --- /dev/null +++ b/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + class ApplicationJob < ActiveJob::Base + end +end diff --git a/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb b/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb new file mode 100644 index 00000000..0ec16fbc --- /dev/null +++ b/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + class ApplicationMailer < ActionMailer::Base + default from: 'from@example.com' + layout 'mailer' + end +end diff --git a/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb b/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb new file mode 100644 index 00000000..598d4397 --- /dev/null +++ b/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + end +end diff --git a/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb b/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb new file mode 100644 index 00000000..0db7b8a5 --- /dev/null +++ b/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb @@ -0,0 +1,16 @@ + + + + Houdini upgrade + <%= csrf_meta_tags %> + <%= csp_meta_tag %> + + <%= stylesheet_link_tag "houdini_upgrade/application", media: "all" %> + <%= javascript_include_tag "houdini_upgrade/application" %> + + + +<%= yield %> + + + diff --git a/gems/houdini_upgrade/bin/rails b/gems/houdini_upgrade/bin/rails new file mode 100755 index 00000000..eb7cc6c7 --- /dev/null +++ b/gems/houdini_upgrade/bin/rails @@ -0,0 +1,24 @@ +#!/usr/bin/env ruby +# This command will automatically be run when you run "rails" with Rails gems +# installed from the root of your application. + +ENGINE_ROOT = File.expand_path('..', __dir__) +ENGINE_PATH = File.expand_path('../lib/houdini_upgrade/engine', __dir__) + +# Set up gems listed in the Gemfile. +ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__) +require "bundler/setup" if File.exist?(ENV["BUNDLE_GEMFILE"]) + +require "rails" +# Pick the frameworks you want: +require "active_model/railtie" +require "active_job/railtie" +require "active_record/railtie" +require "active_storage/engine" +require "action_controller/railtie" +# require "action_mailer/railtie" +require "action_view/railtie" +# require "action_cable/engine" +# require "sprockets/railtie" +# require "rails/test_unit/railtie" +require "rails/engine/commands" diff --git a/gems/houdini_upgrade/config/routes.rb b/gems/houdini_upgrade/config/routes.rb new file mode 100644 index 00000000..9e76a2d0 --- /dev/null +++ b/gems/houdini_upgrade/config/routes.rb @@ -0,0 +1,2 @@ +HoudiniUpgrade::Engine.routes.draw do +end diff --git a/gems/houdini_upgrade/houdini_upgrade.gemspec b/gems/houdini_upgrade/houdini_upgrade.gemspec new file mode 100644 index 00000000..1e41bc70 --- /dev/null +++ b/gems/houdini_upgrade/houdini_upgrade.gemspec @@ -0,0 +1,24 @@ +require_relative "lib/houdini_upgrade/version" + +Gem::Specification.new do |spec| + spec.name = "houdini_upgrade" + spec.version = HoudiniUpgrade::VERSION + spec.authors = ["TODO: Write your name"] + spec.email = ["TODO: Write your email address"] + spec.homepage = "TODO" + spec.summary = "TODO: Summary of HoudiniUpgrade." + spec.description = "TODO: Description of HoudiniUpgrade." + spec.license = "AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later" + + # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' + # to allow pushing to a single host or delete this section to allow pushing to any host. + spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" + + spec.metadata["homepage_uri"] = spec.homepage + spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." + spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + + spec.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "AGPL-3.0.txt", "GPL-3.0.txt", "LGPL-3.0.txt", "Rakefile", "README.md"] + + spec.add_dependency "rails", "~> 5.2.3" +end diff --git a/gems/houdini_upgrade/lib/houdini_upgrade.rb b/gems/houdini_upgrade/lib/houdini_upgrade.rb new file mode 100644 index 00000000..67a8181c --- /dev/null +++ b/gems/houdini_upgrade/lib/houdini_upgrade.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require "houdini_upgrade/engine" + +module HoudiniUpgrade + # Your code goes here... +end diff --git a/gems/houdini_upgrade/lib/houdini_upgrade/engine.rb b/gems/houdini_upgrade/lib/houdini_upgrade/engine.rb new file mode 100644 index 00000000..d4c01f0a --- /dev/null +++ b/gems/houdini_upgrade/lib/houdini_upgrade/engine.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + class Engine < ::Rails::Engine + isolate_namespace HoudiniUpgrade + end +end diff --git a/gems/houdini_upgrade/lib/houdini_upgrade/version.rb b/gems/houdini_upgrade/lib/houdini_upgrade/version.rb new file mode 100644 index 00000000..3f190054 --- /dev/null +++ b/gems/houdini_upgrade/lib/houdini_upgrade/version.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module HoudiniUpgrade + VERSION = '0.1.0' +end diff --git a/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake b/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake new file mode 100644 index 00000000..4b74c0a4 --- /dev/null +++ b/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# desc "Explaining what the task does" +# task :houdini_upgrade do +# # Task goes here +# end From 4eeab0a2841958963a716ce232d2332123f84d5d Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 15:31:34 -0500 Subject: [PATCH 314/440] Remove unneeded files from houdini_upgrade --- .../assets/config/houdini_upgrade_manifest.js | 2 -- .../javascripts/houdini_upgrade/application.js | 15 --------------- .../stylesheets/houdini_upgrade/application.css | 15 --------------- .../houdini_upgrade/application_controller.rb | 8 -------- .../houdini_upgrade/application_helper.rb | 7 ------- .../app/jobs/houdini_upgrade/application_job.rb | 7 ------- .../houdini_upgrade/application_mailer.rb | 9 --------- .../models/houdini_upgrade/application_record.rb | 8 -------- .../layouts/houdini_upgrade/application.html.erb | 16 ---------------- 9 files changed, 87 deletions(-) delete mode 100644 gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js delete mode 100644 gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js delete mode 100644 gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css delete mode 100644 gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb delete mode 100644 gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb delete mode 100644 gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb delete mode 100644 gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb delete mode 100644 gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb delete mode 100644 gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb diff --git a/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js b/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js deleted file mode 100644 index 6c65c4eb..00000000 --- a/gems/houdini_upgrade/app/assets/config/houdini_upgrade_manifest.js +++ /dev/null @@ -1,2 +0,0 @@ -//= link_directory ../javascripts/houdini_upgrade .js -//= link_directory ../stylesheets/houdini_upgrade .css diff --git a/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js b/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js deleted file mode 100644 index 67ce4675..00000000 --- a/gems/houdini_upgrade/app/assets/javascripts/houdini_upgrade/application.js +++ /dev/null @@ -1,15 +0,0 @@ -// This is a manifest file that'll be compiled into application.js, which will include all the files -// listed below. -// -// Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts, -// or any plugin's vendor/assets/javascripts directory can be referenced here using a relative path. -// -// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the -// compiled file. JavaScript code in this file should be added after the last require_* statement. -// -// Read Sprockets README (https://github.com/rails/sprockets#sprockets-directives) for details -// about supported directives. -// -//= require rails-ujs -//= require activestorage -//= require_tree . diff --git a/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css b/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css deleted file mode 100644 index 0ebd7fe8..00000000 --- a/gems/houdini_upgrade/app/assets/stylesheets/houdini_upgrade/application.css +++ /dev/null @@ -1,15 +0,0 @@ -/* - * This is a manifest file that'll be compiled into application.css, which will include all the files - * listed below. - * - * Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets, - * or any plugin's vendor/assets/stylesheets directory can be referenced here using a relative path. - * - * You're free to add application-wide styles to this file and they'll appear at the bottom of the - * compiled file so the styles you add here take precedence over styles defined in any other CSS/SCSS - * files in this directory. Styles in this file should be added after the last require_* statement. - * It is generally better to create a new file per style scope. - * - *= require_tree . - *= require_self - */ diff --git a/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb b/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb deleted file mode 100644 index 27abf6e8..00000000 --- a/gems/houdini_upgrade/app/controllers/houdini_upgrade/application_controller.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module HoudiniUpgrade - class ApplicationController < ActionController::Base - protect_from_forgery with: :exception - end -end diff --git a/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb b/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb deleted file mode 100644 index 392c3d58..00000000 --- a/gems/houdini_upgrade/app/helpers/houdini_upgrade/application_helper.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module HoudiniUpgrade - module ApplicationHelper - end -end diff --git a/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb b/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb deleted file mode 100644 index 2c297597..00000000 --- a/gems/houdini_upgrade/app/jobs/houdini_upgrade/application_job.rb +++ /dev/null @@ -1,7 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module HoudiniUpgrade - class ApplicationJob < ActiveJob::Base - end -end diff --git a/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb b/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb deleted file mode 100644 index 0ec16fbc..00000000 --- a/gems/houdini_upgrade/app/mailers/houdini_upgrade/application_mailer.rb +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module HoudiniUpgrade - class ApplicationMailer < ActionMailer::Base - default from: 'from@example.com' - layout 'mailer' - end -end diff --git a/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb b/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb deleted file mode 100644 index 598d4397..00000000 --- a/gems/houdini_upgrade/app/models/houdini_upgrade/application_record.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module HoudiniUpgrade - class ApplicationRecord < ActiveRecord::Base - self.abstract_class = true - end -end diff --git a/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb b/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb deleted file mode 100644 index 0db7b8a5..00000000 --- a/gems/houdini_upgrade/app/views/layouts/houdini_upgrade/application.html.erb +++ /dev/null @@ -1,16 +0,0 @@ - - - - Houdini upgrade - <%= csrf_meta_tags %> - <%= csp_meta_tag %> - - <%= stylesheet_link_tag "houdini_upgrade/application", media: "all" %> - <%= javascript_include_tag "houdini_upgrade/application" %> - - - -<%= yield %> - - - From 254959f9498eeed8358778ebfdc7e597f3e609d2 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 15:40:21 -0500 Subject: [PATCH 315/440] Correct some issues in the Gemspec --- gems/houdini_upgrade/houdini_upgrade.gemspec | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gems/houdini_upgrade/houdini_upgrade.gemspec b/gems/houdini_upgrade/houdini_upgrade.gemspec index 1e41bc70..e418deec 100644 --- a/gems/houdini_upgrade/houdini_upgrade.gemspec +++ b/gems/houdini_upgrade/houdini_upgrade.gemspec @@ -3,20 +3,20 @@ require_relative "lib/houdini_upgrade/version" Gem::Specification.new do |spec| spec.name = "houdini_upgrade" spec.version = HoudiniUpgrade::VERSION - spec.authors = ["TODO: Write your name"] - spec.email = ["TODO: Write your email address"] - spec.homepage = "TODO" - spec.summary = "TODO: Summary of HoudiniUpgrade." - spec.description = "TODO: Description of HoudiniUpgrade." - spec.license = "AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later" + spec.authors = ["The Houdini Project"] + spec.email = [""] + spec.homepage = "https://houdiniproject.org" + spec.summary = "" + spec.description = "" + spec.license = "AGPL-3.0-or-later WITH WTO-Additional-Permission-3.0-or-later" # Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host' # to allow pushing to a single host or delete this section to allow pushing to any host. spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'" spec.metadata["homepage_uri"] = spec.homepage - spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here." - spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here." + # spec.metadata["source_code_uri"] = "" + # spec.metadata["changelog_uri"] = "" spec.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "AGPL-3.0.txt", "GPL-3.0.txt", "LGPL-3.0.txt", "Rakefile", "README.md"] From 9d28e1e9fd605e158d81f17ab51293795866caad Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 15:40:41 -0500 Subject: [PATCH 316/440] Move the migration for RemoveBillingTiers to houdini_upgrade --- .../db}/migrate/20191105200033_remove_billing_plan_tiers.rb | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename {db => gems/houdini_upgrade/db}/migrate/20191105200033_remove_billing_plan_tiers.rb (100%) diff --git a/db/migrate/20191105200033_remove_billing_plan_tiers.rb b/gems/houdini_upgrade/db/migrate/20191105200033_remove_billing_plan_tiers.rb similarity index 100% rename from db/migrate/20191105200033_remove_billing_plan_tiers.rb rename to gems/houdini_upgrade/db/migrate/20191105200033_remove_billing_plan_tiers.rb From c6e2ede29b730b4f8a3920eabac20fc6f783e767 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 1 May 2020 16:02:15 -0500 Subject: [PATCH 317/440] houdini_upgrade:run will install and run the houdini_upgrade db migrations --- .../lib/tasks/houdini_upgrade_tasks.rake | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake b/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake index 4b74c0a4..c8bdbaed 100644 --- a/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake +++ b/gems/houdini_upgrade/lib/tasks/houdini_upgrade_tasks.rake @@ -1,7 +1,15 @@ # frozen_string_literal: true # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -# desc "Explaining what the task does" -# task :houdini_upgrade do -# # Task goes here -# end + namespace :houdini_upgrade do + Rake::Task["install:migrations"].clear_comments + desc "Run houdini upgrade to v2" + task :run => :environment do + Rake::Task["houdini_upgrade:install:migrations"].invoke + Rake::Task["houdini_upgrade:run_db_migration"].invoke + end + + task :run_db_migration do + sh 'rails db:migrate' + end +end From 14b8e77d3a0102320df851783a268157f9363485 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 4 May 2020 11:19:28 -0500 Subject: [PATCH 318/440] Add the tt extensions to rspec templates because our autoloading breaks if we don't --- .../rspec/channel/{channel_spec.rb.erb => channel_spec.rb.erb.tt} | 0 .../controller/{controller_spec.rb => controller_spec.rb.tt} | 0 .../rspec/controller/{request_spec.rb => request_spec.rb.tt} | 0 .../rspec/controller/{routing_spec.rb => routing_spec.rb.tt} | 0 lib/templates/rspec/controller/{view_spec.rb => view_spec.rb.tt} | 0 .../{feature_singular_spec.rb => feature_singular_spec.rb.tt} | 0 .../rspec/feature/{feature_spec.rb => feature_spec.rb.tt} | 0 .../rspec/generator/{generator_spec.rb => generator_spec.rb.tt} | 0 lib/templates/rspec/helper/{helper_spec.rb => helper_spec.rb.tt} | 0 .../rspec/install/spec/{rails_helper.rb => rails_helper.rb.tt} | 0 .../rspec/integration/{request_spec.rb => request_spec.rb.tt} | 0 lib/templates/rspec/job/{job_spec.rb.erb => job_spec.rb.erb.tt} | 0 .../rspec/mailbox/{mailbox_spec.rb.erb => mailbox_spec.rb.erb.tt} | 0 lib/templates/rspec/mailer/{mailer_spec.rb => mailer_spec.rb.tt} | 0 lib/templates/rspec/mailer/{preview.rb => preview.rb.tt} | 0 lib/templates/rspec/model/{model_spec.rb => model_spec.rb.tt} | 0 .../{api_controller_spec.rb => api_controller_spec.rb.tt} | 0 .../scaffold/{api_request_spec.rb => api_request_spec.rb.tt} | 0 .../rspec/scaffold/{controller_spec.rb => controller_spec.rb.tt} | 0 lib/templates/rspec/scaffold/{edit_spec.rb => edit_spec.rb.tt} | 0 lib/templates/rspec/scaffold/{index_spec.rb => index_spec.rb.tt} | 0 lib/templates/rspec/scaffold/{new_spec.rb => new_spec.rb.tt} | 0 .../rspec/scaffold/{request_spec.rb => request_spec.rb.tt} | 0 .../rspec/scaffold/{routing_spec.rb => routing_spec.rb.tt} | 0 lib/templates/rspec/scaffold/{show_spec.rb => show_spec.rb.tt} | 0 lib/templates/rspec/system/{system_spec.rb => system_spec.rb.tt} | 0 lib/templates/rspec/view/{view_spec.rb => view_spec.rb.tt} | 0 27 files changed, 0 insertions(+), 0 deletions(-) rename lib/templates/rspec/channel/{channel_spec.rb.erb => channel_spec.rb.erb.tt} (100%) rename lib/templates/rspec/controller/{controller_spec.rb => controller_spec.rb.tt} (100%) rename lib/templates/rspec/controller/{request_spec.rb => request_spec.rb.tt} (100%) rename lib/templates/rspec/controller/{routing_spec.rb => routing_spec.rb.tt} (100%) rename lib/templates/rspec/controller/{view_spec.rb => view_spec.rb.tt} (100%) rename lib/templates/rspec/feature/{feature_singular_spec.rb => feature_singular_spec.rb.tt} (100%) rename lib/templates/rspec/feature/{feature_spec.rb => feature_spec.rb.tt} (100%) rename lib/templates/rspec/generator/{generator_spec.rb => generator_spec.rb.tt} (100%) rename lib/templates/rspec/helper/{helper_spec.rb => helper_spec.rb.tt} (100%) rename lib/templates/rspec/install/spec/{rails_helper.rb => rails_helper.rb.tt} (100%) rename lib/templates/rspec/integration/{request_spec.rb => request_spec.rb.tt} (100%) rename lib/templates/rspec/job/{job_spec.rb.erb => job_spec.rb.erb.tt} (100%) rename lib/templates/rspec/mailbox/{mailbox_spec.rb.erb => mailbox_spec.rb.erb.tt} (100%) rename lib/templates/rspec/mailer/{mailer_spec.rb => mailer_spec.rb.tt} (100%) rename lib/templates/rspec/mailer/{preview.rb => preview.rb.tt} (100%) rename lib/templates/rspec/model/{model_spec.rb => model_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{api_controller_spec.rb => api_controller_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{api_request_spec.rb => api_request_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{controller_spec.rb => controller_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{edit_spec.rb => edit_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{index_spec.rb => index_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{new_spec.rb => new_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{request_spec.rb => request_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{routing_spec.rb => routing_spec.rb.tt} (100%) rename lib/templates/rspec/scaffold/{show_spec.rb => show_spec.rb.tt} (100%) rename lib/templates/rspec/system/{system_spec.rb => system_spec.rb.tt} (100%) rename lib/templates/rspec/view/{view_spec.rb => view_spec.rb.tt} (100%) diff --git a/lib/templates/rspec/channel/channel_spec.rb.erb b/lib/templates/rspec/channel/channel_spec.rb.erb.tt similarity index 100% rename from lib/templates/rspec/channel/channel_spec.rb.erb rename to lib/templates/rspec/channel/channel_spec.rb.erb.tt diff --git a/lib/templates/rspec/controller/controller_spec.rb b/lib/templates/rspec/controller/controller_spec.rb.tt similarity index 100% rename from lib/templates/rspec/controller/controller_spec.rb rename to lib/templates/rspec/controller/controller_spec.rb.tt diff --git a/lib/templates/rspec/controller/request_spec.rb b/lib/templates/rspec/controller/request_spec.rb.tt similarity index 100% rename from lib/templates/rspec/controller/request_spec.rb rename to lib/templates/rspec/controller/request_spec.rb.tt diff --git a/lib/templates/rspec/controller/routing_spec.rb b/lib/templates/rspec/controller/routing_spec.rb.tt similarity index 100% rename from lib/templates/rspec/controller/routing_spec.rb rename to lib/templates/rspec/controller/routing_spec.rb.tt diff --git a/lib/templates/rspec/controller/view_spec.rb b/lib/templates/rspec/controller/view_spec.rb.tt similarity index 100% rename from lib/templates/rspec/controller/view_spec.rb rename to lib/templates/rspec/controller/view_spec.rb.tt diff --git a/lib/templates/rspec/feature/feature_singular_spec.rb b/lib/templates/rspec/feature/feature_singular_spec.rb.tt similarity index 100% rename from lib/templates/rspec/feature/feature_singular_spec.rb rename to lib/templates/rspec/feature/feature_singular_spec.rb.tt diff --git a/lib/templates/rspec/feature/feature_spec.rb b/lib/templates/rspec/feature/feature_spec.rb.tt similarity index 100% rename from lib/templates/rspec/feature/feature_spec.rb rename to lib/templates/rspec/feature/feature_spec.rb.tt diff --git a/lib/templates/rspec/generator/generator_spec.rb b/lib/templates/rspec/generator/generator_spec.rb.tt similarity index 100% rename from lib/templates/rspec/generator/generator_spec.rb rename to lib/templates/rspec/generator/generator_spec.rb.tt diff --git a/lib/templates/rspec/helper/helper_spec.rb b/lib/templates/rspec/helper/helper_spec.rb.tt similarity index 100% rename from lib/templates/rspec/helper/helper_spec.rb rename to lib/templates/rspec/helper/helper_spec.rb.tt diff --git a/lib/templates/rspec/install/spec/rails_helper.rb b/lib/templates/rspec/install/spec/rails_helper.rb.tt similarity index 100% rename from lib/templates/rspec/install/spec/rails_helper.rb rename to lib/templates/rspec/install/spec/rails_helper.rb.tt diff --git a/lib/templates/rspec/integration/request_spec.rb b/lib/templates/rspec/integration/request_spec.rb.tt similarity index 100% rename from lib/templates/rspec/integration/request_spec.rb rename to lib/templates/rspec/integration/request_spec.rb.tt diff --git a/lib/templates/rspec/job/job_spec.rb.erb b/lib/templates/rspec/job/job_spec.rb.erb.tt similarity index 100% rename from lib/templates/rspec/job/job_spec.rb.erb rename to lib/templates/rspec/job/job_spec.rb.erb.tt diff --git a/lib/templates/rspec/mailbox/mailbox_spec.rb.erb b/lib/templates/rspec/mailbox/mailbox_spec.rb.erb.tt similarity index 100% rename from lib/templates/rspec/mailbox/mailbox_spec.rb.erb rename to lib/templates/rspec/mailbox/mailbox_spec.rb.erb.tt diff --git a/lib/templates/rspec/mailer/mailer_spec.rb b/lib/templates/rspec/mailer/mailer_spec.rb.tt similarity index 100% rename from lib/templates/rspec/mailer/mailer_spec.rb rename to lib/templates/rspec/mailer/mailer_spec.rb.tt diff --git a/lib/templates/rspec/mailer/preview.rb b/lib/templates/rspec/mailer/preview.rb.tt similarity index 100% rename from lib/templates/rspec/mailer/preview.rb rename to lib/templates/rspec/mailer/preview.rb.tt diff --git a/lib/templates/rspec/model/model_spec.rb b/lib/templates/rspec/model/model_spec.rb.tt similarity index 100% rename from lib/templates/rspec/model/model_spec.rb rename to lib/templates/rspec/model/model_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/api_controller_spec.rb b/lib/templates/rspec/scaffold/api_controller_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/api_controller_spec.rb rename to lib/templates/rspec/scaffold/api_controller_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/api_request_spec.rb b/lib/templates/rspec/scaffold/api_request_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/api_request_spec.rb rename to lib/templates/rspec/scaffold/api_request_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/controller_spec.rb b/lib/templates/rspec/scaffold/controller_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/controller_spec.rb rename to lib/templates/rspec/scaffold/controller_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/edit_spec.rb b/lib/templates/rspec/scaffold/edit_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/edit_spec.rb rename to lib/templates/rspec/scaffold/edit_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/index_spec.rb b/lib/templates/rspec/scaffold/index_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/index_spec.rb rename to lib/templates/rspec/scaffold/index_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/new_spec.rb b/lib/templates/rspec/scaffold/new_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/new_spec.rb rename to lib/templates/rspec/scaffold/new_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/request_spec.rb b/lib/templates/rspec/scaffold/request_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/request_spec.rb rename to lib/templates/rspec/scaffold/request_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/routing_spec.rb b/lib/templates/rspec/scaffold/routing_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/routing_spec.rb rename to lib/templates/rspec/scaffold/routing_spec.rb.tt diff --git a/lib/templates/rspec/scaffold/show_spec.rb b/lib/templates/rspec/scaffold/show_spec.rb.tt similarity index 100% rename from lib/templates/rspec/scaffold/show_spec.rb rename to lib/templates/rspec/scaffold/show_spec.rb.tt diff --git a/lib/templates/rspec/system/system_spec.rb b/lib/templates/rspec/system/system_spec.rb.tt similarity index 100% rename from lib/templates/rspec/system/system_spec.rb rename to lib/templates/rspec/system/system_spec.rb.tt diff --git a/lib/templates/rspec/view/view_spec.rb b/lib/templates/rspec/view/view_spec.rb.tt similarity index 100% rename from lib/templates/rspec/view/view_spec.rb rename to lib/templates/rspec/view/view_spec.rb.tt From 2c1bfd87b299483950904bbfd7c951392f5cd7dc Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 4 May 2020 11:46:30 -0500 Subject: [PATCH 319/440] Correct the script for restoring a database from a pg_dump --- script/pg_restore_local_from_production.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/script/pg_restore_local_from_production.sh b/script/pg_restore_local_from_production.sh index eb9a877d..f8383d3b 100755 --- a/script/pg_restore_local_from_production.sh +++ b/script/pg_restore_local_from_production.sh @@ -1,4 +1,4 @@ #!/bin/bash set -e -pg_restore --verbose --clean --no-acl --no-owner -h db -U admin -d commitchange_development latest.dump +pg_restore --verbose --clean --no-acl --no-owner -h localhost -U houdini_user -d commitchange_development latest.dump From 2c1ec003f79d95b2cc6da4ee7e59cf51f1907cdb Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 7 May 2020 14:24:03 -0500 Subject: [PATCH 320/440] Add .gitkeep to db/migrate --- db/migrate/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100755 db/migrate/.gitkeep diff --git a/db/migrate/.gitkeep b/db/migrate/.gitkeep new file mode 100755 index 00000000..e69de29b From 1379f1d98a0d6670284d73db43ecfbed46412936 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 8 May 2020 14:12:05 -0500 Subject: [PATCH 321/440] Remove unused aws_ses initializer --- config/initializers/aws_ses.rb | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 config/initializers/aws_ses.rb diff --git a/config/initializers/aws_ses.rb b/config/initializers/aws_ses.rb deleted file mode 100644 index 74e49f1c..00000000 --- a/config/initializers/aws_ses.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later - -ActionMailer::Base.add_delivery_method :aws_ses, AWS::SES::Base, - access_key_id: ENV['AWS_ACCESS_KEY'], - secret_access_key: ENV['AWS_SECRET_ACCESS_KEY'], - server: 'email.us-east-1.amazonaws.com' From e99639ffbe8217a64d97449113203908ae9a1230 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 8 May 2020 16:25:43 -0500 Subject: [PATCH 322/440] Remove the migration_sanity_spec.rb since it's not really useful long-term --- spec/migration/migration_sanity_spec.rb | 24 ------------------------ 1 file changed, 24 deletions(-) delete mode 100644 spec/migration/migration_sanity_spec.rb diff --git a/spec/migration/migration_sanity_spec.rb b/spec/migration/migration_sanity_spec.rb deleted file mode 100644 index f135e95a..00000000 --- a/spec/migration/migration_sanity_spec.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'rails_helper' - -describe 'Migration sanity' do - it 'Migrations have a sane timestamp' do - Dir.open(File.join(Rails.root, 'db', 'migrate')) do |dir| - # should be a hash but we don't have in Ruby 2.3 - migration_names = [] - - dir.entries.each do |file| - next unless file != '.' && file != '..' - - ret = file.split('_', 2) - expect(ret[0].length).to eq 14 - expect { Integer(ret[0]) }.to_not raise_error - expect(migration_names).to_not include ret[1] - - migration_names.push(ret[1]) - end - end - end -end From 63f5146655479cfdd13d5dec123eee22a5c6f525 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 11 May 2020 13:38:50 -0500 Subject: [PATCH 323/440] Extract controller related concerns and put them in one place --- app/controllers/api/api_controller.rb | 2 +- app/controllers/application_controller.rb | 72 +----------------- .../billing_subscriptions_controller.rb | 3 +- .../campaign_gift_options_controller.rb | 3 +- .../campaign_gift_options_controller.rb | 3 +- .../campaigns/donations_controller.rb | 3 +- .../campaigns/supporters_controller.rb | 3 +- app/controllers/campaigns_controller.rb | 3 +- .../controllers/campaign/authorization.rb | 19 +++++ .../concerns/controllers/campaign/current.rb | 17 +++++ .../controllers/event/authorization.rb | 25 +++++++ .../concerns/controllers/event/current.rb | 17 +++++ .../concerns/controllers/locale.rb | 18 +++++ .../controllers/nonprofit/authorization.rb | 31 ++++++++ .../concerns/controllers/nonprofit/current.rb | 20 +++++ .../controllers/user/authorization.rb | 73 +++++++++++++++++++ app/controllers/email_settings_controller.rb | 3 +- app/controllers/event_discounts_controller.rb | 3 +- app/controllers/events_controller.rb | 3 +- app/controllers/maps_controller.rb | 3 +- .../nonprofits/activities_controller.rb | 3 +- .../nonprofits/bank_accounts_controller.rb | 3 +- .../nonprofits/button_controller.rb | 3 +- .../nonprofits/cards_controller.rb | 3 +- .../nonprofits/charges_controller.rb | 3 +- .../custom_field_joins_controller.rb | 3 +- .../custom_field_masters_controller.rb | 3 +- .../nonprofits/donations_controller.rb | 3 +- .../nonprofits/email_lists_controller.rb | 3 +- .../nonprofits/imports_controller.rb | 3 +- .../miscellaneous_np_infos_controller.rb | 3 +- .../nonprofits/nonprofit_keys_controller.rb | 3 +- .../nonprofits/payments_controller.rb | 3 +- .../nonprofits/payouts_controller.rb | 3 +- .../recurring_donations_controller.rb | 3 +- .../nonprofits/refunds_controller.rb | 3 +- .../nonprofits/reports_controller.rb | 3 +- .../nonprofits/supporter_emails_controller.rb | 3 +- .../nonprofits/supporter_notes_controller.rb | 3 +- .../nonprofits/supporters_controller.rb | 3 +- .../nonprofits/tag_joins_controller.rb | 3 +- .../nonprofits/tag_masters_controller.rb | 3 +- app/controllers/nonprofits_controller.rb | 3 +- app/controllers/roles_controller.rb | 3 +- app/controllers/settings_controller.rb | 3 +- app/controllers/ticket_levels_controller.rb | 3 +- app/controllers/tickets_controller.rb | 3 +- lib/controllers/campaign_helper.rb | 25 ------- lib/controllers/event_helper.rb | 29 -------- lib/controllers/nonprofit_helper.rb | 53 -------------- 50 files changed, 297 insertions(+), 215 deletions(-) create mode 100644 app/controllers/concerns/controllers/campaign/authorization.rb create mode 100644 app/controllers/concerns/controllers/campaign/current.rb create mode 100644 app/controllers/concerns/controllers/event/authorization.rb create mode 100644 app/controllers/concerns/controllers/event/current.rb create mode 100644 app/controllers/concerns/controllers/locale.rb create mode 100644 app/controllers/concerns/controllers/nonprofit/authorization.rb create mode 100644 app/controllers/concerns/controllers/nonprofit/current.rb create mode 100644 app/controllers/concerns/controllers/user/authorization.rb delete mode 100644 lib/controllers/campaign_helper.rb delete mode 100644 lib/controllers/event_helper.rb delete mode 100644 lib/controllers/nonprofit_helper.rb diff --git a/app/controllers/api/api_controller.rb b/app/controllers/api/api_controller.rb index 44c5e5fa..47769648 100644 --- a/app/controllers/api/api_controller.rb +++ b/app/controllers/api/api_controller.rb @@ -3,7 +3,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Api::ApiController < ApplicationController rescue_from ActiveRecord::RecordInvalid, with: :record_invalid_rescue - + protected def record_invalid_rescue(error) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 44a7d875..3677337f 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -2,23 +2,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class ApplicationController < ActionController::Base + include Controllers::Locale + include Controllers::Nonprofit::Authorization before_action :set_locale, :redirect_to_maintenance - protect_from_forgery - helper_method \ - :current_role?, - :current_nonprofit_user?, - :administered_nonprofit - - def set_locale - if params[:locale] && Settings.available_locales.include?(params[:locale]) - I18n.locale = params[:locale] - else - I18n.locale = Settings.language - end - end - def redirect_to_maintenance if Settings&.maintenance&.maintenance_mode && !current_user unless self.class == Users::SessionsController && @@ -74,62 +62,6 @@ class ApplicationController < ActionController::Base session[:pw_token] == token && Chronic.parse(session[:pw_timestamp]) >= 5.minutes.ago.utc end - def store_location - referrer = request.fullpath - no_redirects = ['/users', '/signup', '/signin', '/users/sign_in', '/users/sign_up', '/users/password', '/users/sign_out', /.*\.json.*/, %r{.*auth/facebook.*}] - unless request.format.symbol == :json || no_redirects.map { |p| referrer.match(p) }.any? - session[:previous_url] = referrer - end - end - - def block_with_sign_in(msg = nil) - store_location - if current_user - flash[:notice] = "It looks like you're not allowed to access that page. If this seems like a mistake, please contact #{Settings.mailer.email}" - redirect_to root_path - else - msg ||= 'We need to sign you in before you can do that.' - redirect_to new_user_session_path, flash: { error: msg } - end - end - - def authenticate_user!(_options = {}) - block_with_sign_in unless current_user - end - - def authenticate_confirmed_user! - if !current_user - block_with_sign_in - elsif !current_user.confirmed? && !current_role?(%i[super_associate super_admin]) - redirect_to new_user_confirmation_path, flash: { error: 'You need to confirm your account to do that.' } - end - end - - def authenticate_super_associate! - unless current_role?(:super_admin) || current_role?(:super_associate) - block_with_sign_in 'Please login.' - end - end - - def authenticate_super_admin! - block_with_sign_in 'Please login.' unless current_role?(:super_admin) - end - - def current_role?(role_names, host_id = nil) - return false unless current_user - - role_names = Array(role_names) - key = "current_role_user_#{current_user_id}_names_#{role_names.join('_')}_host_#{host_id}" - QueryRoles.user_has_role?(current_user.id, role_names, host_id) - end - - def administered_nonprofit - return nil unless current_user - - key = "administered_nonprofit_user_#{current_user_id}_nonprofit" - Nonprofit.where(id: QueryRoles.host_ids(current_user_id, %i[nonprofit_admin nonprofit_associate])).last - end - # devise config def after_sign_in_path_for(_resource) diff --git a/app/controllers/billing_subscriptions_controller.rb b/app/controllers/billing_subscriptions_controller.rb index e13599ed..96d3b517 100644 --- a/app/controllers/billing_subscriptions_controller.rb +++ b/app/controllers/billing_subscriptions_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class BillingSubscriptionsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_admin! diff --git a/app/controllers/campaign_gift_options_controller.rb b/app/controllers/campaign_gift_options_controller.rb index bbd3c140..82745524 100644 --- a/app/controllers/campaign_gift_options_controller.rb +++ b/app/controllers/campaign_gift_options_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignGiftOptionsController < ApplicationController - include Controllers::CampaignHelper + include Controllers::Campaign::Current + include Controllers::Campaign::Authorization before_action :authenticate_campaign_editor!, only: %i[create destroy update update_order] diff --git a/app/controllers/campaigns/campaign_gift_options_controller.rb b/app/controllers/campaigns/campaign_gift_options_controller.rb index e48a5fdd..199391e6 100644 --- a/app/controllers/campaigns/campaign_gift_options_controller.rb +++ b/app/controllers/campaigns/campaign_gift_options_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Campaigns class CampaignGiftOptionsController < ApplicationController - include Controllers::CampaignHelper + include Controllers::Campaign::Current + include Controllers::Campaign::Authorization before_action :authenticate_campaign_editor!, only: %i[create destroy update update_order report] diff --git a/app/controllers/campaigns/donations_controller.rb b/app/controllers/campaigns/donations_controller.rb index 9cb40393..fbc73bc0 100644 --- a/app/controllers/campaigns/donations_controller.rb +++ b/app/controllers/campaigns/donations_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Campaigns class DonationsController < ApplicationController - include Controllers::CampaignHelper + include Controllers::Campaign::Current + include Controllers::Campaign::Authorization before_action :authenticate_campaign_editor!, only: [:index] diff --git a/app/controllers/campaigns/supporters_controller.rb b/app/controllers/campaigns/supporters_controller.rb index a52fb608..d504b446 100644 --- a/app/controllers/campaigns/supporters_controller.rb +++ b/app/controllers/campaigns/supporters_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Campaigns class SupportersController < ApplicationController - include Controllers::CampaignHelper + include Controllers::Campaign::Current + include Controllers::Campaign::Authorization before_action :authenticate_campaign_editor!, only: [:index] diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index d4a9dec0..177ec176 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class CampaignsController < ApplicationController - include Controllers::CampaignHelper + include Controllers::Campaign::Current + include Controllers::Campaign::Authorization helper_method :current_campaign_editor? before_action :authenticate_confirmed_user!, only: %i[create name_and_id duplicate] diff --git a/app/controllers/concerns/controllers/campaign/authorization.rb b/app/controllers/concerns/controllers/campaign/authorization.rb new file mode 100644 index 00000000..e1efe5e5 --- /dev/null +++ b/app/controllers/concerns/controllers/campaign/authorization.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Campaign::Authorization + extend ActiveSupport::Concern + include Controllers::Nonprofit::Authorization + + included do + private + def current_campaign_editor? + !params[:preview] && (current_nonprofit_user? || current_role?(:campaign_editor, current_campaign.id) || current_role?(:super_admin)) + end + def authenticate_campaign_editor! + unless current_campaign_editor? + reject_with_sign_in 'You need to be a campaign editor to do that.' + end + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/campaign/current.rb b/app/controllers/concerns/controllers/campaign/current.rb new file mode 100644 index 00000000..0997124c --- /dev/null +++ b/app/controllers/concerns/controllers/campaign/current.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Campaign::Current + extend ActiveSupport::Concern + include Controllers::Nonprofit::Current + + included do + private + def current_campaign + @campaign ||= FetchCampaign.with_params params, current_nonprofit + raise ActionController::RoutingError, 'Campaign not found' if @campaign.nil? + + @campaign + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/event/authorization.rb b/app/controllers/concerns/controllers/event/authorization.rb new file mode 100644 index 00000000..94e6ca6b --- /dev/null +++ b/app/controllers/concerns/controllers/event/authorization.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Event::Authorization + extend ActiveSupport::Concern + include Controllers::Nonprofit::Authorization + + included do + private + + def current_event_admin? + current_nonprofit_admin? + end + + def current_event_editor? + !params[:preview] && (current_nonprofit_user? || current_role?(:event_editor, current_event.id) || current_role?(:super_admin)) + end + + def authenticate_event_editor! + unless current_event_editor? + reject_with_sign_in 'You need to be the event organizer or a nonprofit administrator before doing that.' + end + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/event/current.rb b/app/controllers/concerns/controllers/event/current.rb new file mode 100644 index 00000000..290b18cb --- /dev/null +++ b/app/controllers/concerns/controllers/event/current.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Event::Current + extend ActiveSupport::Concern + include Controllers::Nonprofit::Current + + included do + private + def current_event + @event ||= FetchEvent.with_params params, current_nonprofit + raise ActionController::RoutingError, 'Event not found' if @event.nil? + + @event + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/locale.rb b/app/controllers/concerns/controllers/locale.rb new file mode 100644 index 00000000..78e2407b --- /dev/null +++ b/app/controllers/concerns/controllers/locale.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Locale + extend ActiveSupport::Concern + + included do + before_action :set_locale + + def set_locale + if params[:locale] && Settings.available_locales.include?(params[:locale]) + I18n.locale = params[:locale] + else + I18n.locale = Settings.language + end + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/nonprofit/authorization.rb b/app/controllers/concerns/controllers/nonprofit/authorization.rb new file mode 100644 index 00000000..1fb3e684 --- /dev/null +++ b/app/controllers/concerns/controllers/nonprofit/authorization.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Nonprofit::Authorization + extend ActiveSupport::Concern + include Controllers::User::Authorization + + included do + private + def authenticate_nonprofit_user!(type: :web) + reject_with_sign_in 'Please sign in' unless current_nonprofit_user? + end + + def authenticate_nonprofit_admin! + reject_with_sign_in 'Please sign in' unless current_nonprofit_admin? + end + + def current_nonprofit_user? + return false if params[:preview] + return false unless current_nonprofit_without_exception + + @current_user_role ||= current_role?(%i[nonprofit_admin nonprofit_associate], current_nonprofit_without_exception.id) || current_role?(:super_admin) + end + + def current_nonprofit_admin? + return false if !current_user || current_user.roles.empty? + + @current_admin_role ||= current_role?(:nonprofit_admin, current_nonprofit.id) || current_role?(:super_admin) + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/nonprofit/current.rb b/app/controllers/concerns/controllers/nonprofit/current.rb new file mode 100644 index 00000000..ce4718db --- /dev/null +++ b/app/controllers/concerns/controllers/nonprofit/current.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::Nonprofit::Current + extend ActiveSupport::Concern + included do + private + def current_nonprofit + @nonprofit = current_nonprofit_without_exception + raise ActionController::RoutingError, 'Nonprofit not found' if @nonprofit.nil? + + @nonprofit + end + + def current_nonprofit_without_exception + key = "current_nonprofit_#{current_user_id}_params_#{[params[:state_code], params[:city], params[:name], params[:nonprofit_id], params[:id]].join('_')}" + FetchNonprofit.with_params params, administered_nonprofit + end + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/user/authorization.rb b/app/controllers/concerns/controllers/user/authorization.rb new file mode 100644 index 00000000..2eb948c4 --- /dev/null +++ b/app/controllers/concerns/controllers/user/authorization.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Controllers::User::Authorization + extend ActiveSupport::Concern + + included do + private + def authenticate_user!(type= :html) + reject_with_sign_in unless current_user + end + + def reject_with_sign_in(msg=nil, type= :html) + if type == :html + block_with_sign_in(msg) + else + render text: msg, status: :unauthorized + end + end + + def block_with_sign_in(msg = nil) + store_location + if current_user + flash[:notice] = "It looks like you're not allowed to access that page. If this seems like a mistake, please contact #{Settings.mailer.email}" + redirect_to root_path + else + msg ||= 'We need to sign you in before you can do that.' + redirect_to new_user_session_path, flash: { error: msg } + end + end + + def current_role?(role_names, host_id = nil) + return false unless current_user + + role_names = Array(role_names) + key = "current_role_user_#{current_user_id}_names_#{role_names.join('_')}_host_#{host_id}" + QueryRoles.user_has_role?(current_user.id, role_names, host_id) + end + + def authenticate_confirmed_user! + if !current_user + reject_with_sign_in + elsif !current_user.confirmed? && !current_role?(%i[super_associate super_admin]) + redirect_to new_user_confirmation_path, flash: { error: 'You need to confirm your account to do that.' } + end + end + + def authenticate_super_associate! + unless current_role?(:super_admin) || current_role?(:super_associate) + reject_with_sign_in 'Please login.' + end + end + + def authenticate_super_admin! + reject_with_sign_in 'Please login.' unless current_role?(:super_admin) + end + + def store_location + referrer = request.fullpath + no_redirects = ['/users', '/signup', '/signin', '/users/sign_in', '/users/sign_up', '/users/password', '/users/sign_out', /.*\.json.*/, %r{.*auth/facebook.*}] + unless request.format.symbol == :json || no_redirects.map { |p| referrer.match(p) }.any? + session[:previous_url] = referrer + end + end + + def administered_nonprofit + return nil unless current_user + + key = "administered_nonprofit_user_#{current_user_id}_nonprofit" + Nonprofit.where(id: QueryRoles.host_ids(current_user_id, %i[nonprofit_admin nonprofit_associate])).last + end + end +end \ No newline at end of file diff --git a/app/controllers/email_settings_controller.rb b/app/controllers/email_settings_controller.rb index 4d14a958..b4652fd1 100644 --- a/app/controllers/email_settings_controller.rb +++ b/app/controllers/email_settings_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EmailSettingsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def index diff --git a/app/controllers/event_discounts_controller.rb b/app/controllers/event_discounts_controller.rb index 103c8b57..9ea04f5e 100644 --- a/app/controllers/event_discounts_controller.rb +++ b/app/controllers/event_discounts_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventDiscountsController < ApplicationController - include Controllers::EventHelper + include Controllers::Event::Current + include Controllers::Event::Authorization before_action :authenticate_event_editor!, except: [:index] def create diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index c0cec3f4..b3522da3 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class EventsController < ApplicationController - include Controllers::EventHelper + include Controllers::Event::Current + include Controllers::Event::Authorization helper_method :current_event_editor? before_action :authenticate_nonprofit_user!, only: :name_and_id diff --git a/app/controllers/maps_controller.rb b/app/controllers/maps_controller.rb index 2982879a..561d11da 100644 --- a/app/controllers/maps_controller.rb +++ b/app/controllers/maps_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class MapsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_super_associate!, only: :all_supporters before_action :authenticate_nonprofit_user!, only: %i[all_npo_supporters specific_npo_supporters] diff --git a/app/controllers/nonprofits/activities_controller.rb b/app/controllers/nonprofits/activities_controller.rb index d5d6b652..a1435e6e 100644 --- a/app/controllers/nonprofits/activities_controller.rb +++ b/app/controllers/nonprofits/activities_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ActivitiesController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! # get /nonprofits/:nonprofit_id/supporters/:supporter_id/activities diff --git a/app/controllers/nonprofits/bank_accounts_controller.rb b/app/controllers/nonprofits/bank_accounts_controller.rb index 74c00eda..3982fa70 100644 --- a/app/controllers/nonprofits/bank_accounts_controller.rb +++ b/app/controllers/nonprofits/bank_accounts_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class BankAccountsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_admin! diff --git a/app/controllers/nonprofits/button_controller.rb b/app/controllers/nonprofits/button_controller.rb index 35fb85a6..caaa866b 100644 --- a/app/controllers/nonprofits/button_controller.rb +++ b/app/controllers/nonprofits/button_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ButtonController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_user! diff --git a/app/controllers/nonprofits/cards_controller.rb b/app/controllers/nonprofits/cards_controller.rb index a8d40c9c..6bd0792b 100644 --- a/app/controllers/nonprofits/cards_controller.rb +++ b/app/controllers/nonprofits/cards_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class CardsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! diff --git a/app/controllers/nonprofits/charges_controller.rb b/app/controllers/nonprofits/charges_controller.rb index 4825b881..232a6c70 100644 --- a/app/controllers/nonprofits/charges_controller.rb +++ b/app/controllers/nonprofits/charges_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ChargesController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user!, only: :index diff --git a/app/controllers/nonprofits/custom_field_joins_controller.rb b/app/controllers/nonprofits/custom_field_joins_controller.rb index 22b4a943..57cbd114 100644 --- a/app/controllers/nonprofits/custom_field_joins_controller.rb +++ b/app/controllers/nonprofits/custom_field_joins_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class CustomFieldJoinsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def index diff --git a/app/controllers/nonprofits/custom_field_masters_controller.rb b/app/controllers/nonprofits/custom_field_masters_controller.rb index 1d846a91..e95691f5 100644 --- a/app/controllers/nonprofits/custom_field_masters_controller.rb +++ b/app/controllers/nonprofits/custom_field_masters_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class CustomFieldMastersController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def index diff --git a/app/controllers/nonprofits/donations_controller.rb b/app/controllers/nonprofits/donations_controller.rb index 79b34459..ea8d4bc9 100644 --- a/app/controllers/nonprofits/donations_controller.rb +++ b/app/controllers/nonprofits/donations_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class DonationsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user!, only: %i[index update] before_action :authenticate_campaign_editor!, only: [:create_offsite] diff --git a/app/controllers/nonprofits/email_lists_controller.rb b/app/controllers/nonprofits/email_lists_controller.rb index b382d0e8..0cb88813 100644 --- a/app/controllers/nonprofits/email_lists_controller.rb +++ b/app/controllers/nonprofits/email_lists_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class EmailListsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! diff --git a/app/controllers/nonprofits/imports_controller.rb b/app/controllers/nonprofits/imports_controller.rb index 87f92d69..dfb212ea 100644 --- a/app/controllers/nonprofits/imports_controller.rb +++ b/app/controllers/nonprofits/imports_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ImportsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! # post /nonprofits/:nonprofit_id/imports diff --git a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb index ecc4ebee..59f1392b 100644 --- a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb +++ b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class MiscellaneousNpInfosController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization helper_method :current_nonprofit_user? before_action :authenticate_nonprofit_user! diff --git a/app/controllers/nonprofits/nonprofit_keys_controller.rb b/app/controllers/nonprofits/nonprofit_keys_controller.rb index 28fdb3f9..62b5023b 100644 --- a/app/controllers/nonprofits/nonprofit_keys_controller.rb +++ b/app/controllers/nonprofits/nonprofit_keys_controller.rb @@ -4,7 +4,8 @@ module Nonprofits class NonprofitKeysController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! # get /nonprofits/:nonprofit_id/nonprofit_keys diff --git a/app/controllers/nonprofits/payments_controller.rb b/app/controllers/nonprofits/payments_controller.rb index 75665c39..cae2eddf 100644 --- a/app/controllers/nonprofits/payments_controller.rb +++ b/app/controllers/nonprofits/payments_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class PaymentsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! diff --git a/app/controllers/nonprofits/payouts_controller.rb b/app/controllers/nonprofits/payouts_controller.rb index 85f3d1f7..047a1197 100644 --- a/app/controllers/nonprofits/payouts_controller.rb +++ b/app/controllers/nonprofits/payouts_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class PayoutsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_admin!, only: :create before_action :authenticate_nonprofit_user!, only: %i[index show] diff --git a/app/controllers/nonprofits/recurring_donations_controller.rb b/app/controllers/nonprofits/recurring_donations_controller.rb index 0a1f2c37..2623484c 100644 --- a/app/controllers/nonprofits/recurring_donations_controller.rb +++ b/app/controllers/nonprofits/recurring_donations_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class RecurringDonationsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user!, except: [:create] diff --git a/app/controllers/nonprofits/refunds_controller.rb b/app/controllers/nonprofits/refunds_controller.rb index 7b0a7232..8afa35e8 100644 --- a/app/controllers/nonprofits/refunds_controller.rb +++ b/app/controllers/nonprofits/refunds_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class RefundsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! diff --git a/app/controllers/nonprofits/reports_controller.rb b/app/controllers/nonprofits/reports_controller.rb index c3c28646..a5fdd6f6 100644 --- a/app/controllers/nonprofits/reports_controller.rb +++ b/app/controllers/nonprofits/reports_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class ReportsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def end_of_year diff --git a/app/controllers/nonprofits/supporter_emails_controller.rb b/app/controllers/nonprofits/supporter_emails_controller.rb index abdaded3..5a3930ac 100644 --- a/app/controllers/nonprofits/supporter_emails_controller.rb +++ b/app/controllers/nonprofits/supporter_emails_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class SupporterEmailsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def create diff --git a/app/controllers/nonprofits/supporter_notes_controller.rb b/app/controllers/nonprofits/supporter_notes_controller.rb index b17f0ea6..1926ea32 100644 --- a/app/controllers/nonprofits/supporter_notes_controller.rb +++ b/app/controllers/nonprofits/supporter_notes_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class SupporterNotesController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user!, except: [:create] diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index 968a7c21..ee2173e7 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class SupportersController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user!, except: %i[new create] diff --git a/app/controllers/nonprofits/tag_joins_controller.rb b/app/controllers/nonprofits/tag_joins_controller.rb index 0a9d2a42..6471927e 100644 --- a/app/controllers/nonprofits/tag_joins_controller.rb +++ b/app/controllers/nonprofits/tag_joins_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class TagJoinsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def index diff --git a/app/controllers/nonprofits/tag_masters_controller.rb b/app/controllers/nonprofits/tag_masters_controller.rb index 07b81cb9..42e234bb 100644 --- a/app/controllers/nonprofits/tag_masters_controller.rb +++ b/app/controllers/nonprofits/tag_masters_controller.rb @@ -3,7 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module Nonprofits class TagMastersController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_user! def index diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index e1d8f989..b9086abf 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization helper_method :current_nonprofit_user? before_action :authenticate_nonprofit_user!, only: %i[dashboard dashboard_metrics dashboard_todos payment_history profile_todos recurring_donation_stats update verify_identity] diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 51d8d054..74c8fad7 100644 --- a/app/controllers/roles_controller.rb +++ b/app/controllers/roles_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class RolesController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization before_action :authenticate_nonprofit_admin! diff --git a/app/controllers/settings_controller.rb b/app/controllers/settings_controller.rb index b35db2f4..f1e31798 100644 --- a/app/controllers/settings_controller.rb +++ b/app/controllers/settings_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class SettingsController < ApplicationController - include Controllers::NonprofitHelper + include Controllers::Nonprofit::Current + include Controllers::Nonprofit::Authorization helper_method :current_nonprofit_user? before_action :authenticate_user! diff --git a/app/controllers/ticket_levels_controller.rb b/app/controllers/ticket_levels_controller.rb index 4c101c17..ab6e7dcb 100644 --- a/app/controllers/ticket_levels_controller.rb +++ b/app/controllers/ticket_levels_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketLevelsController < ApplicationController - include Controllers::EventHelper + include Controllers::Event::Current + include Controllers::Event::Authorization before_action :authenticate_event_editor!, except: %i[index show] diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index c5a1720f..21ed41ec 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -2,7 +2,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class TicketsController < ApplicationController - include Controllers::EventHelper + include Controllers::Event::Current + include Controllers::Event::Authorization helper_method :current_event_admin?, :current_event_editor? before_action :authenticate_event_editor!, except: %i[create add_note] diff --git a/lib/controllers/campaign_helper.rb b/lib/controllers/campaign_helper.rb deleted file mode 100644 index 50b64faf..00000000 --- a/lib/controllers/campaign_helper.rb +++ /dev/null @@ -1,25 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Controllers::CampaignHelper - include Controllers::NonprofitHelper - - private - - def current_campaign - @campaign ||= FetchCampaign.with_params params, current_nonprofit - raise ActionController::RoutingError, 'Campaign not found' if @campaign.nil? - - @campaign - end - - def current_campaign_editor? - !params[:preview] && (current_nonprofit_user? || current_role?(:campaign_editor, current_campaign.id) || current_role?(:super_admin)) - end - - def authenticate_campaign_editor! - unless current_campaign_editor? - block_with_sign_in 'You need to be a campaign editor to do that.' - end - end -end diff --git a/lib/controllers/event_helper.rb b/lib/controllers/event_helper.rb deleted file mode 100644 index 7125436a..00000000 --- a/lib/controllers/event_helper.rb +++ /dev/null @@ -1,29 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Controllers::EventHelper - include Controllers::NonprofitHelper - - private - - def current_event_admin? - current_nonprofit_admin? - end - - def current_event_editor? - !params[:preview] && (current_nonprofit_user? || current_role?(:event_editor, current_event.id) || current_role?(:super_admin)) - end - - def authenticate_event_editor! - unless current_event_editor? - block_with_sign_in 'You need to be the event organizer or a nonprofit administrator before doing that.' - end - end - - def current_event - @event ||= FetchEvent.with_params params, current_nonprofit - raise ActionController::RoutingError, 'Event not found' if @event.nil? - - @event - end -end diff --git a/lib/controllers/nonprofit_helper.rb b/lib/controllers/nonprofit_helper.rb deleted file mode 100644 index 4f36e493..00000000 --- a/lib/controllers/nonprofit_helper.rb +++ /dev/null @@ -1,53 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Controllers::NonprofitHelper - private - - def authenticate_nonprofit_user! - block_with_sign_in 'Please sign in' unless current_nonprofit_user? - end - - def authenticate_nonprofit_admin! - block_with_sign_in 'Please sign in' unless current_nonprofit_admin? - end - - def current_nonprofit_user? - return false if params[:preview] - return false unless current_nonprofit_without_exception - - @current_user_role ||= current_role?(%i[nonprofit_admin nonprofit_associate], current_nonprofit_without_exception.id) || current_role?(:super_admin) - end - - def current_nonprofit_admin? - return false if !current_user || current_user.roles.empty? - - @current_admin_role ||= current_role?(:nonprofit_admin, current_nonprofit.id) || current_role?(:super_admin) - end - - def current_nonprofit - @nonprofit = current_nonprofit_without_exception - raise ActionController::RoutingError, 'Nonprofit not found' if @nonprofit.nil? - - @nonprofit - end - - def current_nonprofit_without_exception - key = "current_nonprofit_#{current_user_id}_params_#{[params[:state_code], params[:city], params[:name], params[:nonprofit_id], params[:id]].join('_')}" - FetchNonprofit.with_params params, administered_nonprofit - end - - def donation_stub - return current_nonprofit_without_exception.donations.last unless current_nonprofit_without_exception.donations.empty? - - OpenStruct.new( - amount: 2000, - created_at: Time.zone.now, - nonprofit: current_nonprofit_without_exception, - campaign: nil, - designation: "Donor's designation here", - dedication: "Donor's dedication here", - id: 1 - ) - end -end From 94f3c13a701b3d8e9c5b4f10b1b122f5d976c58c Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 May 2020 11:27:36 -0500 Subject: [PATCH 324/440] Move profile and user json generation to Jbuilder partials --- app/models/profile.rb | 7 ------- app/models/user.rb | 8 -------- app/views/app_data/_profile.jbuilder | 6 ++++++ app/views/app_data/_user.jbuilder | 8 ++++++++ app/views/layouts/_app_data.html.erb | 4 ++-- 5 files changed, 16 insertions(+), 17 deletions(-) create mode 100644 app/views/app_data/_profile.jbuilder create mode 100644 app/views/app_data/_user.jbuilder diff --git a/app/models/profile.rb b/app/models/profile.rb index 87377ab2..36b0c840 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -102,13 +102,6 @@ class Profile < ApplicationRecord Rails.application.routes.url_helpers.profile_path(self) end - def as_json(options = {}) - h = super(options) - h[:pic_tiny] = get_profile_picture :tiny - h[:url] = url - h - end - # Cache setters def set_caches! diff --git a/app/models/user.rb b/app/models/user.rb index b32cb218..397edd83 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -84,14 +84,6 @@ class User < ApplicationRecord false end - def as_json(options = {}) - h = super(options) - h[:unconfirmed_email] = unconfirmed_email - h[:confirmed] = confirmed? - h[:profile] = profile.as_json - h - end - # This is useful for manually generating a Devise user confirmation token so that we can get the confirmation URL with the correct token from anywhere def make_confirmation_token! raw, db = Devise.token_generator.generate(User, :confirmation_token) diff --git a/app/views/app_data/_profile.jbuilder b/app/views/app_data/_profile.jbuilder new file mode 100644 index 00000000..96068f15 --- /dev/null +++ b/app/views/app_data/_profile.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.extract! profile, :id, :name, :country, :picture +json.url profile_path(profile) +json.pic_tiny profile.get_profile_pic(:tiny) \ No newline at end of file diff --git a/app/views/app_data/_user.jbuilder b/app/views/app_data/_user.jbuilder new file mode 100644 index 00000000..6eeef093 --- /dev/null +++ b/app/views/app_data/_user.jbuilder @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.extract! user, :id, :created_at, :updated_at +json.unconfirmed_email user.unconfirmed_email +json.confirmed user.confirmed? + +json.partial! render 'app_data/profile', profile: user.profile diff --git a/app/views/layouts/_app_data.html.erb b/app/views/layouts/_app_data.html.erb index bb4f3d72..7ca3a9b6 100644 --- a/app/views/layouts/_app_data.html.erb +++ b/app/views/layouts/_app_data.html.erb @@ -7,9 +7,9 @@ var app = { , current_admin: <%= !!(current_user && current_role?(:super_admin)) %> , nonprofit: <%= @nonprofit ? raw(@nonprofit.to_json(root:false)) : 'undefined' %> , nonprofit_id : <%= @nonprofit ? @nonprofit.id : 'undefined' %> -, user: <%= current_user ? raw(current_user.to_json(root:false)) : 'undefined' %> +, user: <%= current_user ? raw("render('app_data/user', user: current_user)") : 'undefined' %> , user_id: <%= current_user ? current_user.id : 'undefined' %> -, profile: <%= current_user ? raw(current_user.profile.to_json(root:false)) : 'undefined' %> +, profile: <%= current_user ? raw("render('app_data/profile', profile: current_user.profile)") : 'undefined' %> , profile_id: <%= current_user ? current_user.profile.id : 'undefined' %> , asset_path: "<%= Rails.application.config.assets.prefix %>" , host_with_port: "//<%= request.host_with_port %>" From c2c166dd1b4646362de2575d5f2d7ab6b8803b8a Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 May 2020 11:28:28 -0500 Subject: [PATCH 325/440] Fix bug in carrierwave initializer due to a carrierwave upgrade --- config/initializers/carrierwave.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb index 301b8c4b..1784b8b4 100755 --- a/config/initializers/carrierwave.rb +++ b/config/initializers/carrierwave.rb @@ -11,6 +11,6 @@ CarrierWave.configure do |config| config.aws_credentials = { access_key_id: Settings.aws.access_key_id, secret_access_key: Settings.aws.secret_access_key, - config: AWS.config(cache_dir: "#{Rails.root}/tmp/uploads", region: Settings.aws.region) + region: Settings.aws.region } end From 1b68d6e04cac8c27b08c007359a832425f5c014d Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 May 2020 11:54:28 -0500 Subject: [PATCH 326/440] Correct render bugs with the user and profile json --- app/views/app_data/_profile.jbuilder | 2 +- app/views/app_data/_user.jbuilder | 2 +- app/views/layouts/_app_data.html.erb | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/app/views/app_data/_profile.jbuilder b/app/views/app_data/_profile.jbuilder index 96068f15..76d36c6a 100644 --- a/app/views/app_data/_profile.jbuilder +++ b/app/views/app_data/_profile.jbuilder @@ -3,4 +3,4 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later json.extract! profile, :id, :name, :country, :picture json.url profile_path(profile) -json.pic_tiny profile.get_profile_pic(:tiny) \ No newline at end of file +json.pic_tiny profile.get_profile_picture(:tiny) \ No newline at end of file diff --git a/app/views/app_data/_user.jbuilder b/app/views/app_data/_user.jbuilder index 6eeef093..ceae5f67 100644 --- a/app/views/app_data/_user.jbuilder +++ b/app/views/app_data/_user.jbuilder @@ -5,4 +5,4 @@ json.extract! user, :id, :created_at, :updated_at json.unconfirmed_email user.unconfirmed_email json.confirmed user.confirmed? -json.partial! render 'app_data/profile', profile: user.profile +json.partial! 'app_data/profile.json', profile: user.profile diff --git a/app/views/layouts/_app_data.html.erb b/app/views/layouts/_app_data.html.erb index 7ca3a9b6..cf1c1014 100644 --- a/app/views/layouts/_app_data.html.erb +++ b/app/views/layouts/_app_data.html.erb @@ -7,10 +7,10 @@ var app = { , current_admin: <%= !!(current_user && current_role?(:super_admin)) %> , nonprofit: <%= @nonprofit ? raw(@nonprofit.to_json(root:false)) : 'undefined' %> , nonprofit_id : <%= @nonprofit ? @nonprofit.id : 'undefined' %> -, user: <%= current_user ? raw("render('app_data/user', user: current_user)") : 'undefined' %> +, user: <%= current_user ? raw(render('app_data/user.json', user: current_user)) : 'undefined' %> , user_id: <%= current_user ? current_user.id : 'undefined' %> -, profile: <%= current_user ? raw("render('app_data/profile', profile: current_user.profile)") : 'undefined' %> -, profile_id: <%= current_user ? current_user.profile.id : 'undefined' %> +, profile: <%= current_user&.profile ? raw(render('app_data/profile.json', profile: current_user.profile)) : 'undefined' %> +, profile_id: <%= current_user&.profile ? current_user.profile.id : 'undefined' %> , asset_path: "<%= Rails.application.config.assets.prefix %>" , host_with_port: "//<%= request.host_with_port %>" , google_api: "<%= ENV['GOOGLE_API_KEY'] %>" From c499f8f7a5d34a2901680bc49e407dd1a0619f41 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 May 2020 14:06:14 -0500 Subject: [PATCH 327/440] Fix bugs in profile views caused by webpacker conversion --- app/javascript/packs/page__profiles__fundraisers.js | 1 + app/javascript/packs/page__profiles__show.js | 1 + app/views/profiles/fundraisers.html.erb | 4 ++++ app/views/profiles/show.html.erb | 4 +++- 4 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 app/javascript/packs/page__profiles__fundraisers.js create mode 100644 app/javascript/packs/page__profiles__show.js diff --git a/app/javascript/packs/page__profiles__fundraisers.js b/app/javascript/packs/page__profiles__fundraisers.js new file mode 100644 index 00000000..10da02c3 --- /dev/null +++ b/app/javascript/packs/page__profiles__fundraisers.js @@ -0,0 +1 @@ +require('../legacy/page.js') \ No newline at end of file diff --git a/app/javascript/packs/page__profiles__show.js b/app/javascript/packs/page__profiles__show.js new file mode 100644 index 00000000..10da02c3 --- /dev/null +++ b/app/javascript/packs/page__profiles__show.js @@ -0,0 +1 @@ +require('../legacy/page.js') \ No newline at end of file diff --git a/app/views/profiles/fundraisers.html.erb b/app/views/profiles/fundraisers.html.erb index aae509c1..ac2935b7 100644 --- a/app/views/profiles/fundraisers.html.erb +++ b/app/views/profiles/fundraisers.html.erb @@ -3,6 +3,10 @@ - Donor Fundraisers (P2P) <% end %> +<%= content_for :javascripts do %> + <%= javascript_pack_tag 'i18n', 'page__profiles__fundraisers'%> +<% end %> + <% content_for(:footer_hidden) {'hidden'} %> <% content_for :stylesheets do %> diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 79932515..486fe7b7 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -2,7 +2,9 @@ <% content_for :title_suffix do %> - Donor Profile <% end %> - +<%= content_for :javascripts do %> + <%= javascript_pack_tag 'i18n', 'page__profiles__show'%> +<% end %> <% content_for :stylesheets do %> <%= stylesheet_link_tag 'profiles/show/page' %> <% end %> From 25a64abe3f3d0bbb62626f467f34cb4d337150b9 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 14 May 2020 14:32:49 -0500 Subject: [PATCH 328/440] Remove bizarre unneeded profile call --- app/controllers/nonprofits_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index b9086abf..a268f762 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -19,7 +19,6 @@ class NonprofitsController < ApplicationController @nonprofit = current_nonprofit @url = Format::Url.concat(root_url, @nonprofit.url) @supporters = @nonprofit.supporters.not_deleted - @profiles = @nonprofit.profiles.order('total_raised DESC').limit(5).includes(:user).uniq events = @nonprofit.events.not_deleted.order('start_datetime desc') campaigns = @nonprofit.campaigns.not_deleted.not_a_child.order('created_at desc') From 480ac12c64b5454562957d77f7c18a46f560a5da Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 15 May 2020 14:54:32 -0500 Subject: [PATCH 329/440] Fix the donations history page due to the webpacker migration --- app/javascript/packs/page__profiles__donations_history.js | 1 + app/views/profiles/donations_history.html.erb | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 app/javascript/packs/page__profiles__donations_history.js diff --git a/app/javascript/packs/page__profiles__donations_history.js b/app/javascript/packs/page__profiles__donations_history.js new file mode 100644 index 00000000..10da02c3 --- /dev/null +++ b/app/javascript/packs/page__profiles__donations_history.js @@ -0,0 +1 @@ +require('../legacy/page.js') \ No newline at end of file diff --git a/app/views/profiles/donations_history.html.erb b/app/views/profiles/donations_history.html.erb index abd4db3e..65d6cc77 100755 --- a/app/views/profiles/donations_history.html.erb +++ b/app/views/profiles/donations_history.html.erb @@ -3,6 +3,10 @@ - Donor Donations <% end %> +<%= content_for :javascripts do %> + <%= javascript_pack_tag 'i18n', 'page__profiles__donations_history'%> +<% end %> + <% content_for(:footer_hidden) {'hidden'} %> <% content_for :stylesheets do %> From 72e78babd94fd7c7952e307f56558fe317179454 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 15 May 2020 16:05:58 -0500 Subject: [PATCH 330/440] Add current_nonprofit_user? as a helper_method --- app/controllers/concerns/controllers/nonprofit/current.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/controllers/concerns/controllers/nonprofit/current.rb b/app/controllers/concerns/controllers/nonprofit/current.rb index ce4718db..14940031 100644 --- a/app/controllers/concerns/controllers/nonprofit/current.rb +++ b/app/controllers/concerns/controllers/nonprofit/current.rb @@ -4,7 +4,8 @@ module Controllers::Nonprofit::Current extend ActiveSupport::Concern included do - private + helper_method :current_nonprofit_user? + def current_nonprofit @nonprofit = current_nonprofit_without_exception raise ActionController::RoutingError, 'Nonprofit not found' if @nonprofit.nil? From f84fef200cb8234e2e86929959c8fd324d0d00c4 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 15 May 2020 17:00:47 -0500 Subject: [PATCH 331/440] Fix bugs in ordering of javascript loads on events/show --- app/views/events/show.html.erb | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 40b8568f..91c555f9 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -21,17 +21,19 @@ <%= content_for :javascripts do %> <%= render 'common/froala' if current_event_editor? %> <%= render 'schema', event: @event, url: @url %> <%= javascript_pack_tag 'i18n', 'page__events__show' %> + <% end %> <%= content_for :head do %> From ea5196ceb1918475e4b0bcf3094acd25cb7af8d0 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 19 May 2020 14:55:01 -0500 Subject: [PATCH 332/440] Remove old client js --- client/js/bank_accounts/confirm/index.es6 | 75 - client/js/bank_accounts/confirm/page.js | 2 - client/js/bank_accounts/create.es6 | 75 - .../resend_confirmation_email.js | 11 - client/js/campaigns/index/page.js | 4 - .../js/campaigns/new/peer_to_peer_wizard.js | 43 - client/js/campaigns/new/wizard.js | 57 - client/js/campaigns/peer_to_peer/page.js | 129 - client/js/campaigns/show/admin.js | 113 - .../show/choose-gift-options-modal.js | 44 - .../js/campaigns/show/gift-option-button.js | 57 - client/js/campaigns/show/gift-option-list.js | 81 - .../show/gift-option-quantity-left.js | 18 - client/js/campaigns/show/is-sold-out.js | 3 - .../show/metrics-and-contribute-box.js | 105 - client/js/campaigns/show/page.js | 169 - client/js/campaigns/show/tour.js | 35 - .../js/campaigns/supporters/index/index.es6 | 63 - client/js/campaigns/supporters/index/meta.es6 | 27 - .../js/campaigns/supporters/index/metrics.es6 | 35 - client/js/campaigns/supporters/index/page.js | 5 - .../supporters/index/supporter-list.es6 | 29 - .../supporters/index/supporter-table.es6 | 69 - client/js/campaigns/timeline.js | 93 - client/js/campaigns/totals.js | 15 - client/js/cards/create-frp.es6 | 11 - client/js/cards/create.js | 129 - .../ajax/check_campaign_or_event_name.js | 16 - .../get_campaign_and_event_names_and_ids.js | 36 - client/js/common/application_view.js | 393 -- client/js/common/apply-pikaday.js | 14 - client/js/common/autosubmit.js | 72 - client/js/common/brand-fonts.js | 8 - client/js/common/class-object.js | 8 - client/js/common/client.js | 25 - client/js/common/colors.js | 59 - client/js/common/confirmation.js | 46 - client/js/common/credit-card-validator.js | 25 - client/js/common/css-gradient.js | 8 - client/js/common/direct-to-s3-upload.es6 | 41 - client/js/common/dynamic_form.js | 30 - client/js/common/editable.js | 10 - client/js/common/editor/froala.es6 | 150 - client/js/common/editor/quill.es6 | 45 - client/js/common/el_swapo.js | 30 - client/js/common/event.js | 14 - client/js/common/ff-form-validation/index.es6 | 180 - .../ff-form-validation/lib/currency-regex.es6 | 3 - .../ff-form-validation/lib/email-regex.es6 | 4 - .../ff-form-validation/lib/readable-prop.es6 | 12 - client/js/common/file-input-stream.js | 17 - client/js/common/form-to-object.js | 44 - client/js/common/form.js | 20 - client/js/common/format.js | 126 - client/js/common/format_response_error.js | 20 - client/js/common/fundraiser_metrics.js | 16 - client/js/common/geography.js | 15 - client/js/common/get-valid-data.js | 11 - client/js/common/image_uploader.js | 34 - client/js/common/jquery_additions.js | 32 - client/js/common/notification.js | 13 - client/js/common/on-change-sanitize-slug.js | 11 - client/js/common/on-ios11.js | 11 - client/js/common/onboard.js | 280 -- client/js/common/panels_layout.js | 71 - client/js/common/pikaday-timepicker.js | 30 - client/js/common/polyfills.js | 11 - client/js/common/post-form-data.es6 | 26 - client/js/common/post-form-data.js | 27 - client/js/common/request.js | 11 - client/js/common/restful_resource.js | 139 - client/js/common/sanitize-slug.js | 5 - client/js/common/scroll_toggle_class.js | 32 - client/js/common/search-data.js | 52 - client/js/common/super-agent-frp.js | 33 - client/js/common/super-agent-promise.js | 41 - client/js/common/time-remaining.js | 39 - client/js/common/utilities.js | 180 - client/js/common/vendor/Chart.min.js | 3476 ----------------- .../vendor/bootstrap-tour-standalone.js | 1288 ------ client/js/common/vendor/bootstrap.js | 334 -- client/js/common/vendor/colpick.js | 517 --- client/js/common/vendor/jquery.cookie.js | 110 - client/js/common/vendor/masonry.js | 9 - client/js/components/activity_feed.js | 3 - .../components/address-autocomplete-fields.js | 96 - client/js/components/address-autocomplete.js | 81 - .../js/components/ajax/toggle_soft_delete.js | 15 - client/js/components/b64.js | 13 - client/js/components/branded_fundraising.js | 10 - client/js/components/card-form.es6 | 190 - client/js/components/chart-options.js | 29 - client/js/components/checkbox.js | 15 - client/js/components/color-picker.es6 | 32 - client/js/components/confirmation-modal.js | 44 - client/js/components/date-range.js | 11 - client/js/components/date_range_picker.js | 32 - client/js/components/dollar-input.js | 15 - client/js/components/drag-to-reorder.js | 37 - client/js/components/duplicate_fundraiser.js | 26 - client/js/components/encode-plain-email.js | 20 - client/js/components/field-with-error.js | 14 - .../fundraising/add_header_image.js | 6 - client/js/components/maps/cc_map.js | 110 - client/js/components/maps/default_options.js | 14 - client/js/components/maps/npo_coordinates.js | 9 - client/js/components/maps/styles.js | 269 -- client/js/components/modal.js | 9 - client/js/components/nonprofit-branding.js | 5 - client/js/components/number-input.js | 17 - client/js/components/progress-bar.js | 23 - client/js/components/public-activities.js | 64 - .../js/components/radio-and-label-wrapper.js | 11 - client/js/components/radios.js | 24 - client/js/components/render-activities.js | 18 - client/js/components/saving_indicator.js | 19 - client/js/components/search-table.js | 43 - client/js/components/search.js | 10 - client/js/components/select.js | 23 - client/js/components/sepa-form.es6 | 110 - client/js/components/set-state-from-value.js | 18 - client/js/components/show-more-button.es6 | 35 - client/js/components/state-selector.js | 25 - client/js/components/styles/branded-wizard.js | 71 - client/js/components/styles/render-styles.js | 9 - .../js/components/supporter-address-form.es6 | 88 - client/js/components/supporter-fields.js | 170 - .../tables/filtering/apply_filter.js | 131 - client/js/components/tables/search.es6 | 51 - client/js/components/text-input.js | 15 - client/js/components/textarea.js | 15 - client/js/components/todos.js | 26 - client/js/components/top-nav.js | 10 - client/js/components/wizard.js | 59 - client/js/donations/create.js | 49 - client/js/donations/create_offline.js | 24 - client/js/events/discounts/index.js | 37 - client/js/events/discounts/manage.js | 93 - client/js/events/index/page.js | 8 - client/js/events/listing-item/index.js | 64 - client/js/events/listings/index.js | 57 - client/js/events/new/wizard.js | 59 - client/js/events/show/editor.js | 43 - client/js/events/show/event_donation.js | 27 - client/js/events/show/page.js | 111 - client/js/events/show/tour.js | 35 - client/js/events/stats/page.js | 99 - client/js/gift_options/admin.js | 101 - client/js/gift_options/index.js | 33 - client/js/nonprofits/btn/page.js | 19 - client/js/nonprofits/button/amounts.js | 60 - client/js/nonprofits/button/appearance.js | 94 - client/js/nonprofits/button/designations.js | 86 - client/js/nonprofits/button/footer.js | 11 - .../js/nonprofits/button/hide-dedication.js | 31 - client/js/nonprofits/button/page.js | 196 - client/js/nonprofits/button/preview.js | 159 - client/js/nonprofits/button/thank-you.js | 29 - client/js/nonprofits/button/type.js | 41 - client/js/nonprofits/cards/edit/index.es6 | 61 - client/js/nonprofits/cards/edit/page.js | 4 - client/js/nonprofits/dashboard/page.js | 83 - client/js/nonprofits/dashboard/tour.js | 68 - client/js/nonprofits/donate/amount-step.js | 199 - .../js/nonprofits/donate/dedication-form.js | 97 - client/js/nonprofits/donate/followup-step.js | 39 - client/js/nonprofits/donate/get-params.js | 23 - client/js/nonprofits/donate/info-step.js | 187 - client/js/nonprofits/donate/page.js | 85 - client/js/nonprofits/donate/payment-step.js | 175 - .../plugins-available/alwaysAnonymous.js | 11 - .../plugins-available/default-recurring.js | 9 - .../donate/plugins-available/dummy.js | 16 - .../donate/plugins-available/ibanonly.js | 5 - .../donate/plugins-available/minamount.js | 4 - .../donate/plugins-available/minimalForm.js | 20 - .../donate/plugins-available/piwik.js | 23 - .../plugins-available/prefill-identity.js | 23 - .../donate/plugins-available/prettify.js | 9 - .../donate/plugins-available/select-amount.js | 15 - client/js/nonprofits/donate/wizard.js | 208 - client/js/nonprofits/edit/page.js | 8 - client/js/nonprofits/payments/index/page.js | 102 - .../payments/index/payment_details.js | 166 - client/js/nonprofits/payments/index/tour.js | 45 - client/js/nonprofits/payments_chart.js | 111 - client/js/nonprofits/payouts/create.js | 18 - .../index/identity-verification-form.es6 | 277 -- client/js/nonprofits/payouts/index/page.js | 27 - .../payouts/index/verify_identity.js | 24 - .../recurring_donations/index/create.js | 122 - .../recurring_donations/index/delete.js | 11 - .../recurring_donations/index/index.es6 | 74 - .../recurring_donations/index/page.js | 50 - .../recurring_donations/index/tour.js | 40 - .../recurring_donations/index/update.js | 15 - .../recurring_donations/readable_interval.js | 13 - client/js/nonprofits/reports/modal.js | 58 - client/js/nonprofits/show/page.js | 88 - client/js/nonprofits/show/tour.js | 25 - client/js/nonprofits/supporter_form/index.es6 | 36 - client/js/nonprofits/supporter_form/page.js | 2 - client/js/nonprofits/supporters/create.js | 17 - client/js/nonprofits/supporters/get_name.js | 5 - .../js/nonprofits/supporters/import/index.es6 | 262 -- .../import/regex-header-matchers.js | 121 - .../supporters/index/action_recipient.js | 11 - .../supporters/index/bulk_delete.js | 40 - .../js/nonprofits/supporters/index/import.js | 9 - .../supporters/index/list_supporters.js | 112 - .../supporters/index/manage_custom_fields.js | 124 - .../supporters/index/manage_tags.js | 137 - .../supporters/index/merge_supporters.js | 79 - client/js/nonprofits/supporters/index/page.js | 34 - .../index/sidepanel/generate-content.js | 128 - .../supporters/index/sidepanel/index.js | 147 - .../index/sidepanel/offsite-donation-form.js | 33 - .../index/sidepanel/supporter-actions.js | 25 - .../index/sidepanel/supporter-activities.js | 74 - .../index/sidepanel/supporter-note-form.js | 95 - .../supporters/index/supporter_details.js | 183 - .../index/tags_and_fields_shared_methods.js | 59 - .../nonprofits/supporters/index/timeline.js | 69 - client/js/nonprofits/supporters/index/tour.js | 86 - client/js/nonprofits/supporters/new/page.js | 24 - client/js/page.js | 75 - client/js/pages/show/index.js | 5 - .../recurring_donations/edit/amount-step.es6 | 111 - .../edit/branded-wizard.es6 | 51 - .../js/recurring_donations/edit/card-form.es6 | 186 - .../edit/change-amount-wizard.es6 | 132 - .../edit/custom-nonprofit-branding.es6 | 4 - .../recurring_donations/edit/followup-step.js | 21 - .../js/recurring_donations/edit/get-params.js | 23 - client/js/recurring_donations/edit/index.es6 | 384 -- client/js/recurring_donations/edit/page.js | 2 - .../recurring_donations/edit/payment-step.es6 | 105 - client/js/recurring_donations/index.js | 15 - client/js/refunds/create.js | 69 - client/js/settings/index/branding/index.js | 53 - client/js/settings/index/branding/view.js | 79 - .../js/settings/index/email-settings/index.js | 45 - .../js/settings/index/email-settings/view.js | 61 - .../js/settings/index/integrations/index.js | 60 - client/js/settings/index/page.js | 134 - client/js/stripe_wrapper/index.es6 | 54 - client/js/stripe_wrapper/page.js | 2 - client/js/super-admin/fullcontact-table.js | 11 - client/js/super-admin/nonprofits-table.js | 67 - client/js/super-admin/page.js | 48 - client/js/super-admin/profiles-table.js | 48 - client/js/supporters/index.js | 67 - client/js/supporters/info-card.es6 | 111 - .../mailchimp-integration-settings.js | 86 - client/js/ticket_levels/get_totals.js | 11 - client/js/ticket_levels/manage.js | 82 - client/js/tickets/index/delete-ticket.js | 32 - client/js/tickets/index/page.js | 195 - client/js/tickets/new.js | 32 - client/js/tickets/wizard.js | 150 - client/js/widget/donate-button.v2.js | 237 -- 261 files changed, 20961 deletions(-) delete mode 100644 client/js/bank_accounts/confirm/index.es6 delete mode 100644 client/js/bank_accounts/confirm/page.js delete mode 100755 client/js/bank_accounts/create.es6 delete mode 100644 client/js/bank_accounts/resend_confirmation_email.js delete mode 100644 client/js/campaigns/index/page.js delete mode 100644 client/js/campaigns/new/peer_to_peer_wizard.js delete mode 100644 client/js/campaigns/new/wizard.js delete mode 100644 client/js/campaigns/peer_to_peer/page.js delete mode 100644 client/js/campaigns/show/admin.js delete mode 100644 client/js/campaigns/show/choose-gift-options-modal.js delete mode 100644 client/js/campaigns/show/gift-option-button.js delete mode 100644 client/js/campaigns/show/gift-option-list.js delete mode 100644 client/js/campaigns/show/gift-option-quantity-left.js delete mode 100644 client/js/campaigns/show/is-sold-out.js delete mode 100644 client/js/campaigns/show/metrics-and-contribute-box.js delete mode 100755 client/js/campaigns/show/page.js delete mode 100644 client/js/campaigns/show/tour.js delete mode 100644 client/js/campaigns/supporters/index/index.es6 delete mode 100644 client/js/campaigns/supporters/index/meta.es6 delete mode 100644 client/js/campaigns/supporters/index/metrics.es6 delete mode 100644 client/js/campaigns/supporters/index/page.js delete mode 100644 client/js/campaigns/supporters/index/supporter-list.es6 delete mode 100644 client/js/campaigns/supporters/index/supporter-table.es6 delete mode 100644 client/js/campaigns/timeline.js delete mode 100644 client/js/campaigns/totals.js delete mode 100644 client/js/cards/create-frp.es6 delete mode 100644 client/js/cards/create.js delete mode 100644 client/js/common/ajax/check_campaign_or_event_name.js delete mode 100644 client/js/common/ajax/get_campaign_and_event_names_and_ids.js delete mode 100644 client/js/common/application_view.js delete mode 100644 client/js/common/apply-pikaday.js delete mode 100644 client/js/common/autosubmit.js delete mode 100644 client/js/common/brand-fonts.js delete mode 100644 client/js/common/class-object.js delete mode 100644 client/js/common/client.js delete mode 100644 client/js/common/colors.js delete mode 100644 client/js/common/confirmation.js delete mode 100644 client/js/common/credit-card-validator.js delete mode 100644 client/js/common/css-gradient.js delete mode 100644 client/js/common/direct-to-s3-upload.es6 delete mode 100644 client/js/common/dynamic_form.js delete mode 100644 client/js/common/editable.js delete mode 100644 client/js/common/editor/froala.es6 delete mode 100644 client/js/common/editor/quill.es6 delete mode 100644 client/js/common/el_swapo.js delete mode 100644 client/js/common/event.js delete mode 100644 client/js/common/ff-form-validation/index.es6 delete mode 100644 client/js/common/ff-form-validation/lib/currency-regex.es6 delete mode 100644 client/js/common/ff-form-validation/lib/email-regex.es6 delete mode 100644 client/js/common/ff-form-validation/lib/readable-prop.es6 delete mode 100644 client/js/common/file-input-stream.js delete mode 100644 client/js/common/form-to-object.js delete mode 100644 client/js/common/form.js delete mode 100644 client/js/common/format.js delete mode 100644 client/js/common/format_response_error.js delete mode 100644 client/js/common/fundraiser_metrics.js delete mode 100644 client/js/common/geography.js delete mode 100644 client/js/common/get-valid-data.js delete mode 100644 client/js/common/image_uploader.js delete mode 100644 client/js/common/jquery_additions.js delete mode 100644 client/js/common/notification.js delete mode 100644 client/js/common/on-change-sanitize-slug.js delete mode 100644 client/js/common/on-ios11.js delete mode 100644 client/js/common/onboard.js delete mode 100644 client/js/common/panels_layout.js delete mode 100644 client/js/common/pikaday-timepicker.js delete mode 100644 client/js/common/polyfills.js delete mode 100644 client/js/common/post-form-data.es6 delete mode 100644 client/js/common/post-form-data.js delete mode 100644 client/js/common/request.js delete mode 100644 client/js/common/restful_resource.js delete mode 100644 client/js/common/sanitize-slug.js delete mode 100644 client/js/common/scroll_toggle_class.js delete mode 100644 client/js/common/search-data.js delete mode 100644 client/js/common/super-agent-frp.js delete mode 100644 client/js/common/super-agent-promise.js delete mode 100644 client/js/common/time-remaining.js delete mode 100755 client/js/common/utilities.js delete mode 100755 client/js/common/vendor/Chart.min.js delete mode 100644 client/js/common/vendor/bootstrap-tour-standalone.js delete mode 100644 client/js/common/vendor/bootstrap.js delete mode 100644 client/js/common/vendor/colpick.js delete mode 100755 client/js/common/vendor/jquery.cookie.js delete mode 100644 client/js/common/vendor/masonry.js delete mode 100644 client/js/components/activity_feed.js delete mode 100644 client/js/components/address-autocomplete-fields.js delete mode 100644 client/js/components/address-autocomplete.js delete mode 100644 client/js/components/ajax/toggle_soft_delete.js delete mode 100644 client/js/components/b64.js delete mode 100644 client/js/components/branded_fundraising.js delete mode 100644 client/js/components/card-form.es6 delete mode 100644 client/js/components/chart-options.js delete mode 100644 client/js/components/checkbox.js delete mode 100644 client/js/components/color-picker.es6 delete mode 100644 client/js/components/confirmation-modal.js delete mode 100644 client/js/components/date-range.js delete mode 100644 client/js/components/date_range_picker.js delete mode 100644 client/js/components/dollar-input.js delete mode 100644 client/js/components/drag-to-reorder.js delete mode 100644 client/js/components/duplicate_fundraiser.js delete mode 100644 client/js/components/encode-plain-email.js delete mode 100644 client/js/components/field-with-error.js delete mode 100644 client/js/components/fundraising/add_header_image.js delete mode 100644 client/js/components/maps/cc_map.js delete mode 100644 client/js/components/maps/default_options.js delete mode 100644 client/js/components/maps/npo_coordinates.js delete mode 100644 client/js/components/maps/styles.js delete mode 100644 client/js/components/modal.js delete mode 100644 client/js/components/nonprofit-branding.js delete mode 100644 client/js/components/number-input.js delete mode 100644 client/js/components/progress-bar.js delete mode 100644 client/js/components/public-activities.js delete mode 100644 client/js/components/radio-and-label-wrapper.js delete mode 100644 client/js/components/radios.js delete mode 100644 client/js/components/render-activities.js delete mode 100644 client/js/components/saving_indicator.js delete mode 100644 client/js/components/search-table.js delete mode 100644 client/js/components/search.js delete mode 100644 client/js/components/select.js delete mode 100644 client/js/components/sepa-form.es6 delete mode 100644 client/js/components/set-state-from-value.js delete mode 100644 client/js/components/show-more-button.es6 delete mode 100644 client/js/components/state-selector.js delete mode 100644 client/js/components/styles/branded-wizard.js delete mode 100644 client/js/components/styles/render-styles.js delete mode 100644 client/js/components/supporter-address-form.es6 delete mode 100644 client/js/components/supporter-fields.js delete mode 100644 client/js/components/tables/filtering/apply_filter.js delete mode 100644 client/js/components/tables/search.es6 delete mode 100644 client/js/components/text-input.js delete mode 100644 client/js/components/textarea.js delete mode 100644 client/js/components/todos.js delete mode 100644 client/js/components/top-nav.js delete mode 100644 client/js/components/wizard.js delete mode 100644 client/js/donations/create.js delete mode 100644 client/js/donations/create_offline.js delete mode 100644 client/js/events/discounts/index.js delete mode 100644 client/js/events/discounts/manage.js delete mode 100644 client/js/events/index/page.js delete mode 100644 client/js/events/listing-item/index.js delete mode 100644 client/js/events/listings/index.js delete mode 100644 client/js/events/new/wizard.js delete mode 100644 client/js/events/show/editor.js delete mode 100644 client/js/events/show/event_donation.js delete mode 100644 client/js/events/show/page.js delete mode 100644 client/js/events/show/tour.js delete mode 100644 client/js/events/stats/page.js delete mode 100644 client/js/gift_options/admin.js delete mode 100644 client/js/gift_options/index.js delete mode 100644 client/js/nonprofits/btn/page.js delete mode 100644 client/js/nonprofits/button/amounts.js delete mode 100644 client/js/nonprofits/button/appearance.js delete mode 100644 client/js/nonprofits/button/designations.js delete mode 100644 client/js/nonprofits/button/footer.js delete mode 100644 client/js/nonprofits/button/hide-dedication.js delete mode 100644 client/js/nonprofits/button/page.js delete mode 100644 client/js/nonprofits/button/preview.js delete mode 100644 client/js/nonprofits/button/thank-you.js delete mode 100644 client/js/nonprofits/button/type.js delete mode 100644 client/js/nonprofits/cards/edit/index.es6 delete mode 100644 client/js/nonprofits/cards/edit/page.js delete mode 100644 client/js/nonprofits/dashboard/page.js delete mode 100644 client/js/nonprofits/dashboard/tour.js delete mode 100644 client/js/nonprofits/donate/amount-step.js delete mode 100644 client/js/nonprofits/donate/dedication-form.js delete mode 100644 client/js/nonprofits/donate/followup-step.js delete mode 100644 client/js/nonprofits/donate/get-params.js delete mode 100644 client/js/nonprofits/donate/info-step.js delete mode 100644 client/js/nonprofits/donate/page.js delete mode 100644 client/js/nonprofits/donate/payment-step.js delete mode 100644 client/js/nonprofits/donate/plugins-available/alwaysAnonymous.js delete mode 100644 client/js/nonprofits/donate/plugins-available/default-recurring.js delete mode 100644 client/js/nonprofits/donate/plugins-available/dummy.js delete mode 100644 client/js/nonprofits/donate/plugins-available/ibanonly.js delete mode 100644 client/js/nonprofits/donate/plugins-available/minamount.js delete mode 100644 client/js/nonprofits/donate/plugins-available/minimalForm.js delete mode 100644 client/js/nonprofits/donate/plugins-available/piwik.js delete mode 100644 client/js/nonprofits/donate/plugins-available/prefill-identity.js delete mode 100644 client/js/nonprofits/donate/plugins-available/prettify.js delete mode 100644 client/js/nonprofits/donate/plugins-available/select-amount.js delete mode 100644 client/js/nonprofits/donate/wizard.js delete mode 100644 client/js/nonprofits/edit/page.js delete mode 100755 client/js/nonprofits/payments/index/page.js delete mode 100644 client/js/nonprofits/payments/index/payment_details.js delete mode 100644 client/js/nonprofits/payments/index/tour.js delete mode 100644 client/js/nonprofits/payments_chart.js delete mode 100644 client/js/nonprofits/payouts/create.js delete mode 100644 client/js/nonprofits/payouts/index/identity-verification-form.es6 delete mode 100644 client/js/nonprofits/payouts/index/page.js delete mode 100644 client/js/nonprofits/payouts/index/verify_identity.js delete mode 100644 client/js/nonprofits/recurring_donations/index/create.js delete mode 100644 client/js/nonprofits/recurring_donations/index/delete.js delete mode 100644 client/js/nonprofits/recurring_donations/index/index.es6 delete mode 100644 client/js/nonprofits/recurring_donations/index/page.js delete mode 100644 client/js/nonprofits/recurring_donations/index/tour.js delete mode 100644 client/js/nonprofits/recurring_donations/index/update.js delete mode 100644 client/js/nonprofits/recurring_donations/readable_interval.js delete mode 100644 client/js/nonprofits/reports/modal.js delete mode 100755 client/js/nonprofits/show/page.js delete mode 100644 client/js/nonprofits/show/tour.js delete mode 100644 client/js/nonprofits/supporter_form/index.es6 delete mode 100644 client/js/nonprofits/supporter_form/page.js delete mode 100644 client/js/nonprofits/supporters/create.js delete mode 100644 client/js/nonprofits/supporters/get_name.js delete mode 100644 client/js/nonprofits/supporters/import/index.es6 delete mode 100644 client/js/nonprofits/supporters/import/regex-header-matchers.js delete mode 100644 client/js/nonprofits/supporters/index/action_recipient.js delete mode 100644 client/js/nonprofits/supporters/index/bulk_delete.js delete mode 100644 client/js/nonprofits/supporters/index/import.js delete mode 100644 client/js/nonprofits/supporters/index/list_supporters.js delete mode 100644 client/js/nonprofits/supporters/index/manage_custom_fields.js delete mode 100644 client/js/nonprofits/supporters/index/manage_tags.js delete mode 100644 client/js/nonprofits/supporters/index/merge_supporters.js delete mode 100644 client/js/nonprofits/supporters/index/page.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/generate-content.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/index.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/offsite-donation-form.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/supporter-actions.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/supporter-activities.js delete mode 100644 client/js/nonprofits/supporters/index/sidepanel/supporter-note-form.js delete mode 100644 client/js/nonprofits/supporters/index/supporter_details.js delete mode 100644 client/js/nonprofits/supporters/index/tags_and_fields_shared_methods.js delete mode 100644 client/js/nonprofits/supporters/index/timeline.js delete mode 100644 client/js/nonprofits/supporters/index/tour.js delete mode 100644 client/js/nonprofits/supporters/new/page.js delete mode 100644 client/js/page.js delete mode 100644 client/js/pages/show/index.js delete mode 100644 client/js/recurring_donations/edit/amount-step.es6 delete mode 100644 client/js/recurring_donations/edit/branded-wizard.es6 delete mode 100644 client/js/recurring_donations/edit/card-form.es6 delete mode 100644 client/js/recurring_donations/edit/change-amount-wizard.es6 delete mode 100644 client/js/recurring_donations/edit/custom-nonprofit-branding.es6 delete mode 100644 client/js/recurring_donations/edit/followup-step.js delete mode 100644 client/js/recurring_donations/edit/get-params.js delete mode 100644 client/js/recurring_donations/edit/index.es6 delete mode 100644 client/js/recurring_donations/edit/page.js delete mode 100644 client/js/recurring_donations/edit/payment-step.es6 delete mode 100644 client/js/recurring_donations/index.js delete mode 100644 client/js/refunds/create.js delete mode 100644 client/js/settings/index/branding/index.js delete mode 100644 client/js/settings/index/branding/view.js delete mode 100644 client/js/settings/index/email-settings/index.js delete mode 100644 client/js/settings/index/email-settings/view.js delete mode 100644 client/js/settings/index/integrations/index.js delete mode 100644 client/js/settings/index/page.js delete mode 100644 client/js/stripe_wrapper/index.es6 delete mode 100644 client/js/stripe_wrapper/page.js delete mode 100644 client/js/super-admin/fullcontact-table.js delete mode 100644 client/js/super-admin/nonprofits-table.js delete mode 100644 client/js/super-admin/page.js delete mode 100644 client/js/super-admin/profiles-table.js delete mode 100644 client/js/supporters/index.js delete mode 100644 client/js/supporters/info-card.es6 delete mode 100644 client/js/supporters/settings/mailchimp-integration-settings.js delete mode 100644 client/js/ticket_levels/get_totals.js delete mode 100644 client/js/ticket_levels/manage.js delete mode 100644 client/js/tickets/index/delete-ticket.js delete mode 100644 client/js/tickets/index/page.js delete mode 100644 client/js/tickets/new.js delete mode 100644 client/js/tickets/wizard.js delete mode 100644 client/js/widget/donate-button.v2.js diff --git a/client/js/bank_accounts/confirm/index.es6 b/client/js/bank_accounts/confirm/index.es6 deleted file mode 100644 index 39365009..00000000 --- a/client/js/bank_accounts/confirm/index.es6 +++ /dev/null @@ -1,75 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('virtual-dom/h') -const request = require('../../common/super-agent-frp') -const view = require('vvvview') -const flyd = require('flyd') -const flatMap = require('flyd/module/flatmap') -const thunk = require('vdom-thunk') -const format = require('../../common/format') -const Im = require('immutable') -const fromJS = Im.fromJS -const Map = Im.Map - -var npURL = '/nonprofits/' + app.nonprofit_id - - -const root = state => { - var headerContent = '' - if(state.get('loading')) { - headerContent = [h('i.fa.fa-spin.fa-gear'), ' Confirming Bank Account...'] - } else if(!state.get('loading') && !state.get('pending_verification')) { - headerContent = [h('i.fa.fa-check'), ' Bank Account Confirmed!'] - } else { // not loading and unable to confirm - headerContent = ['Unable to confirm bank account.'] - } - - var confirmedMsg = !state.get('pending_verification') - ? h('p', [ - 'Your bank account connection has been confirmed with your email address. ', - h("br"), - h('a', {href: npURL + '/payouts'}, [h('i.fa.fa-return'), 'Return to your payouts dashboard']) - ]) - : '' - - return h('div', [ - h('h2', headerContent), - confirmedMsg, - thunk(accountInfo, state), - h('hr'), - h('p', [ - 'If any of this looks incorrect, please contact: ', - h('a', {href: 'mailto:support@commitchange.com'}, 'support@commitchange.com') - ]) - ]) -} - - -const accountInfo = state => - h('div.well', [ - h('p', ['Nonprofit: ', h('strong', state.getIn(['nonprofit', 'name'])), ]), - h('p', ['New bank account: ', h('strong', state.get('name')), ]), - h('p', ['User who made the change: ', h('strong', state.get('email')), ]), - h('p', ['Date and time of update: ', h('strong', format.date.toSimple(state.get('created_at'))), ]), - ]) - - -var state = fromJS(app.bankAccount).set('loading', true) - -var confirmView = view(root, document.querySelector('.js-view-confirm'), state) - - -if(app.bankAccount.pending_verification) { - var $confirmResponse = request.post(npURL + '/bank_account/confirm') - .send({token: utils.get_param('t')}) - .perform() - - var $state = flyd.scan( - (state, resp) => state.set('loading', false).set('pending_verification', false) - , state - , $confirmResponse) - - flyd.map(confirmView, $state) -} else { - confirmView(state.set('loading', false).set('pending_verification', false)) -} - diff --git a/client/js/bank_accounts/confirm/page.js b/client/js/bank_accounts/confirm/page.js deleted file mode 100644 index a3d1fc6e..00000000 --- a/client/js/bank_accounts/confirm/page.js +++ /dev/null @@ -1,2 +0,0 @@ -// License: LGPL-3.0-or-later -require('./index.es6') diff --git a/client/js/bank_accounts/create.es6 b/client/js/bank_accounts/create.es6 deleted file mode 100755 index c86d1054..00000000 --- a/client/js/bank_accounts/create.es6 +++ /dev/null @@ -1,75 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../common/super-agent-promise') -var format_err = require('../common/format_response_error') - -module.exports = create_bank_account - -function create_bank_account(form_data, el) { - return new Promise((resolve, reject) =>{ - appl.def('new_bank_account', {loading: true, error: ''}) - return confirm_auth(form_data) - .then(tokenize_with_stripe) - .then(create_record) - .then(complete) - .catch(display_err) - }) -} - - -// Post to confirm user's password -function confirm_auth(form_data) { - - return request.post('/users/confirm_auth').send({password: form_data.user_password}) - .perform() - .then((resp) =>{ return {token: resp.body.token, form: form_data}}) - - -} - - -// Post to stripe to get back a stripe_bank_account_token -function tokenize_with_stripe(data) { - return new Promise(function(resolve, reject) { - Stripe.bankAccount.createToken(data.form, function(status, resp) { - data.stripe_resp = resp - if(resp.error) reject(resp.error.message) - else resolve(data) - }) - }) -} - - -// 'data' must have a stripe response as '.stripe_resp' and a user password confirmation token as '.token -function create_record(data) { - return request.post('/nonprofits/' + app.nonprofit_id + '/bank_account') - .send({ - pw_token: data.token, - bank_account: { - stripe_bank_account_token: data.stripe_resp.id, - stripe_bank_account_id: data.stripe_resp.bank_account.id, - name: data.stripe_resp.bank_account.bank_name + ' *' + data.stripe_resp.bank_account.last4, - email: app.user.email - } - }) - .perform() -} - -function complete() { - appl.is_loading() - appl.reload() -} - -function display_err(resp) { - - var error_message = null; - - if (typeof resp == 'string') - error_message = resp - else - error_message = format_err(resp) - - appl.def('new_bank_account', {error: error_message, loading: false}) -} - - - diff --git a/client/js/bank_accounts/resend_confirmation_email.js b/client/js/bank_accounts/resend_confirmation_email.js deleted file mode 100644 index 5eb6843a..00000000 --- a/client/js/bank_accounts/resend_confirmation_email.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../common/super-agent-frp') - -var a = document.querySelector(".js-event-resendBankConfirmEmail") - -if(a) a.addEventListener('click', resendBankConfirmation) - -function resendBankConfirmation() { - request.post('/nonprofits/' + app.nonprofit_id + '/bank_account/resend_confirmation').perform() - appl.open_modal('bankConfirmResendModal') -} diff --git a/client/js/campaigns/index/page.js b/client/js/campaigns/index/page.js deleted file mode 100644 index 2d0166dd..00000000 --- a/client/js/campaigns/index/page.js +++ /dev/null @@ -1,4 +0,0 @@ -// License: LGPL-3.0-or-later -if(app.user) - require('../new/wizard') - diff --git a/client/js/campaigns/new/peer_to_peer_wizard.js b/client/js/campaigns/new/peer_to_peer_wizard.js deleted file mode 100644 index c839d09c..00000000 --- a/client/js/campaigns/new/peer_to_peer_wizard.js +++ /dev/null @@ -1,43 +0,0 @@ -// License: LGPL-3.0-or-later - -//This is used for federated p2p campaigns -require('../../components/wizard') -var format_err = require('../../common/format_response_error') - -appl.def('advance_p2p_campaign_name_step', function(form_obj) { - var name = form_obj['campaign[name]'] - appl.def('new_p2p_campaign', form_obj) - appl.wizard.advance('new_p2p_campaign_wiz') -}) - -// Post a new campaign. -appl.def('create_p2p_campaign', function(el) { - var form_data = utils.toFormData(appl.prev_elem(el)) - form_data = utils.mergeFormData(form_data, appl.new_p2p_campaign) - appl.def('new_p2p_campaign_wiz.loading', true) - - post_p2p_campaign(form_data) - .then(function(req) { - appl.notify("Redirecting to your campaign...") - appl.redirect(JSON.parse(req.response).url) - }) - .catch(function(req) { - appl.def('new_p2p_campaign_wiz.loading', false) - appl.def('new_p2p_campaign_wiz.error', req.responseText) - }) -}) - -// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image -function post_p2p_campaign(form_data) { - return new Promise(function(resolve, reject) { - var req = new XMLHttpRequest() - req.open("POST", '/nonprofits/' + app.nonprofit_id + '/campaigns') - req.setRequestHeader('X-CSRF-Token', window._csrf) - console.log(form_data) - req.send(form_data) - req.onload = function(ev) { - if(req.status === 200) resolve(req) - else reject(req) - } - }) -} diff --git a/client/js/campaigns/new/wizard.js b/client/js/campaigns/new/wizard.js deleted file mode 100644 index ab107da1..00000000 --- a/client/js/campaigns/new/wizard.js +++ /dev/null @@ -1,57 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/pikaday-timepicker') -require('../../components/wizard') -require('../../common/image_uploader') -var checkName = require('../../common/ajax/check_campaign_or_event_name') -var format_err = require('../../common/format_response_error') - - -appl.def('advance_campaign_name_step', function(form_obj) { - var name = form_obj['campaign[name]'] - checkName(name, 'campaign', function(){ - appl.def('new_campaign', form_obj) - appl.wizard.advance('new_campaign_wiz') - }) -}) - -// Post a new campaign. -appl.def('create_campaign', function(el) { - var form_data = utils.toFormData(appl.prev_elem(el)) - form_data = utils.mergeFormData(form_data, appl.new_campaign) - appl.def('new_campaign_wiz.loading', true) - -// TODO: for p2p capmaigns, merge with preset campaing params - - post_campaign(form_data) - .then(function(req) { - appl.notify("Redirecting to your campaign...") - appl.redirect(JSON.parse(req.response).url) - }) - .catch(function(req) { - appl.def('new_campaign_wiz.loading', false) - appl.def('new_campaign_wiz.error', req.responseText) - }) -}) - - -var Pikaday = require('pikaday') -var moment = require('moment') -new Pikaday({ - field: document.querySelector('.js-date-picker'), - format: 'M/D/YYYY', - minDate: moment().toDate() -}) - -// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image -function post_campaign(form_data) { - return new Promise(function(resolve, reject) { - var req = new XMLHttpRequest() - req.open("POST", '/nonprofits/' + app.nonprofit_id + '/campaigns') - req.setRequestHeader('X-CSRF-Token', window._csrf) - req.send(form_data) - req.onload = function(ev) { - if(req.status === 200) resolve(req) - else reject(req) - } - }) -} diff --git a/client/js/campaigns/peer_to_peer/page.js b/client/js/campaigns/peer_to_peer/page.js deleted file mode 100644 index c9730bcf..00000000 --- a/client/js/campaigns/peer_to_peer/page.js +++ /dev/null @@ -1,129 +0,0 @@ -// License: LGPL-3.0-or-later -require('../new/peer_to_peer_wizard') -require('../new/wizard.js') -require('../../common/image_uploader') - -var request = require("../../common/client") - -appl.def('undelete_p2p', function (url){ - appl.def('loading', true) - request.put(url + '/soft_delete', {delete: false}).end(function(err, resp) { - if (err) { - appl.def('loading', false) - } - else{ - window.location = url - } - - }) -}) - -// setting up some default values -appl.def('is_signing_up', true) - .def('selected_result_index', -1) - - -appl.def('search_nonprofits', function(value){ - // keyCode 13 is the return key. - // this conditional just clears the dropdown - if(event.keyCode === 13) { - appl.def('search_results', []) - return - } - // when the user starts typing, - // it sets the selected_results key to false - appl.def('selected_result', false) - - // if the the input is empty, it clears the dropdown - if (!value) { - appl.def('search_results', []) - return - } - - // logic for controlling the dropdown options with up - // and down arrows - if (returnUpOrDownArrow() && appl.search_results && appl.search_results.length) { - event.preventDefault() - setIndexWithArrows(returnUpOrDownArrow()) - return - } - - // if the input is not an up or down arrow or an empty string - // or a return key, then it searches for nonprofits - utils.delay(300, function(){ajax_nonprofit_search(value)}) -}) - - -function ajax_nonprofit_search(value){ - request.get('/nonprofits/search?npo_name=' + value).end(function(err, resp){ - if(!resp.body) { - appl.def('search_results', []) - appl.notify("Sorry, we couldn't find any nonprofits containing the word '" + value + "'") - } else { - appl.def('selected_result_index', -1) - appl.def('search_results', resp.body) - } - }) -} - - -function returnUpOrDownArrow() { - var keyCode = event.keyCode - if(keyCode === 38) - return 'up' - if(keyCode === 40) - return 'down' -} - - -function setIndexWithArrows(dir) { - if(dir === 'down') { - var search_length = appl.search_results.length -1 - appl.def('selected_result_index', appl.selected_result_index === search_length - ? search_length - : appl.selected_result_index += 1) - } else { - appl.def('selected_result_index', appl.selected_result_index === 0 - ? 0 - : appl.selected_result_index -= 1) - } -} - -appl.def('select_result', { - with_arrows: function(i, node) { - addSelectedClass(appl.prev_elem(node)) - var selected = appl.search_results[appl.selected_result_index] - app.nonprofit_id = selected.id - appl.def('selected_result', selected) - utils.change_url_param('npo_id', selected.id, '/peer-to-peer') - }, - with_click: function(i, node) { - appl.def('selected_result_index', i) - addSelectedClass(appl.prev_elem(node)) - var selected = appl.search_results[i] - app.nonprofit_id = selected.id - appl.def('selected_result', selected) - appl.def('search_results', []) - utils.change_url_param('npo_id', selected.id, '/peer-to-peer') - } -}) - - -function addSelectedClass(node) { - if(!node || !node.parentElement) return - var siblings = node.parentElement.querySelectorAll('li') - var len = siblings.length - while(len--){siblings[len].className=''} - node.className = 'is-selected' -} - -// this is for clearing the dropdown -var main = document.querySelector('main') - -main.onclick = function(ev) { - var node = ev.target.nodeName - if(node === 'INPUT' || node === 'BUTTON') { - return - } - appl.def('search_results', []) -} diff --git a/client/js/campaigns/show/admin.js b/client/js/campaigns/show/admin.js deleted file mode 100644 index c4703b95..00000000 --- a/client/js/campaigns/show/admin.js +++ /dev/null @@ -1,113 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/pikaday-timepicker') -require('../../common/restful_resource') -const request = require('../../common/client') -const formatErr = require('../../common/format_response_error') -require('../../common/image_uploader') -require('./tour') -const dupeIt = require('../../components/duplicate_fundraiser') - -dupeIt(`/nonprofits/${app.nonprofit_id}/campaigns`, app.campaign_id) - -var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id -var create_supporter = require('../../nonprofits/supporters/create') -var create_offline_donation = require('../../donations/create_offline') - -require('../../components/ajax/toggle_soft_delete')(url, 'campaign') - -// Initialize the froala wysiwyg -var editable = require('../../common/editable') -if (app.is_parent_campaign) { - editable($('#js-campaignBody'), { - sticky: true, - placeholder: "Add your campaign's story here. We strongly recommend that this section is filled out with at least 250 words. It will be saved automatically as you type. You can add images, videos and custom HTML too." - }) -} - -editable($('#js-customReceipt'), { - button: ["bold", "italic", "formatBlock", "align", "createLink", - "insertImage", "insertUnorderedList", "insertOrderedList", - "undo", "redo", "insert_donate_button", "html"] - , placeholder: "Add optional message here. It will be saved automatically as you type." -}) - - - -var path = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id - - -appl.def('remove_banner_image', function() { - var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id - var notification = 'Removing banner image...' - var payload = {remove_banner_image : true} - appl.remove_image(url, 'campaign', notification, payload) -}) - -appl.def('remove_background_image', function() { - var url = '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id - var notification = 'Removing background image...' - var payload = {remove_background_image : true} - appl.remove_image(url, 'campaign', notification, payload) -}) - -appl.def('count_story_words', function() { - var wysiwyg = document.querySelector(".editable") - appl.def('has_story', wysiwyg.textContent.split(' ').length > 60) -}) - -appl.def('highlight_body', function(){ - appl.def('body_is_highlighted', true) - appl.close_modal() -}) - -appl.count_story_words(document.querySelector('.campaign-body')) - -appl.def('track_launch', function() { - window.location.reload() -}) - -appl.def('create_offline_donation', function(data, el) { - create_supporter({supporter: data.supporter}, createSupporterUI) - .then(function(resp) { - data.supporter_id = resp.body.id - delete data.supporter - return create_offline_donation(data, createDonationUI) - }).then(function(el){ - appl.ajax_metrics.index() - appl.prev_elem(el).reset() - }) -}) - - -var createSupporterUI = { - start: function() { - appl.is_loading() - }, - success: function() { - return this - }, - fail: function(msg) { - appl.def('error', formatErr(msg)) - appl.not_loading() - } -} - -var createDonationUI = { - start: function() { }, - success: function(resp) { - appl.not_loading() - appl.close_modal() - appl.notify('Campaign Donation Saved!') - }, - fail: function(msg){ - appl.def('error', formatErr(msg)) - appl.not_loading() - } -} - -if(app.vimeo_id) { - request.get('http://vimeo.com/api/v2/video/' + app.vimeo_id + '.json') - .end(function(err, resp){ - appl.def('vimeo_image_url', "background-image:url('" + resp.body[0].thumbnail_small + "')") - }) -} diff --git a/client/js/campaigns/show/choose-gift-options-modal.js b/client/js/campaigns/show/choose-gift-options-modal.js deleted file mode 100644 index 2b42675a..00000000 --- a/client/js/campaigns/show/choose-gift-options-modal.js +++ /dev/null @@ -1,44 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const flyd = require('flyd') -const R = require('ramda') - -const soldOut = require('./is-sold-out') -const giftButton = require('./gift-option-button') - -const giftOption = g => h('option', { props: {value: g.id} }, g.name) - -const setDisplayGift = (state, gifts) => ev => { - var id = Number(ev.target.value) - state.selectedModalGift$(R.find(R.propEq('id', id))(gifts)) -} - -const chooseGift = (state, gifts) => - h('div.pastelBox--grey.u-padding--10', [ - h('select.u-margin--0', {on: {change: setDisplayGift(state, gifts)}} - , R.concat([h('option', 'Choose a gift option')], R.map(giftOption, gifts))) - , h('div.sideGifts', - state.selectedModalGift$() && state.selectedModalGift$().id - ? [ - h('p.u-marginTop--10', state.selectedModalGift$().description || '') - , giftButton(state.giftOptions, state.selectedModalGift$()) - ] - : '' - ) - ]) - -const regularContribution = state => { - if (app.campaign.hide_custom_amounts) return '' - return h('div.u-marginTop--15.centered', [ - h('a', {on: {click: state.clickRegularContribution$}}, 'Contribute with no gift option') - ]) -} - -module.exports = state => { - var gifts = R.filter(g => !soldOut(g), state.giftOptions.giftOptions$() || []) - return h('div.u-padding--15', [ - chooseGift(state, gifts) - , regularContribution(state) - ]) -} - diff --git a/client/js/campaigns/show/gift-option-button.js b/client/js/campaigns/show/gift-option-button.js deleted file mode 100644 index b4c67a9d..00000000 --- a/client/js/campaigns/show/gift-option-button.js +++ /dev/null @@ -1,57 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const branding = require('../../components/nonprofit-branding') -const format = require('../../common/format') -const soldOut = require('./is-sold-out') - -// function prepareForIOS11() -// { -// bad_elements = $('.ff-modalBackdrop') -// for(var i = 0; i < bad_elements.length; i++) -// { -// bad_elements[i].classList.add('ios-force-absolute-positioning') -// } -// -// -// $('body').scrollTop(195) // so incredibly hacky -// } - - -module.exports = (state, gift) => { - if(state.timeRemaining$() <= 0) return '' // dont show gift options button if the campaign has ended - return h('table', { - class: {'u-hide': !gift.amount_one_time && !gift.amount_recurring} - }, [ - h('tr', [ - gift.amount_one_time - ? h('td', [ - h('button.button--small.button--gift', { - on: {click: ev => { - - - state.clickOption$([gift, gift.amount_one_time, 'one-time'])} - } - - , style: {background: branding.dark} - , props: {title: `Contribute towards ${gift.name}`} - , class: {disabled: soldOut(gift)} - }, [ h('span.dollar', '$ ') , format.centsToDollars(gift.amount_one_time), h('br'), h('small', 'One-time') ]) - ]) - : '' // no one-time amount - , gift.amount_recurring && gift.amount_one_time ? h('td.orWithLine') : '' // whether to show the cool OR graphic between buttons - , gift.amount_recurring - ? h('td', [ - h('button.button--small.button--gift', { - on: {click: ev => { - - state.clickOption$([gift, gift.amount_recurring, 'recurring'])} - } - , style: {background: branding.dark} - , props: {title: `Contribute monthly towards ${gift.name}`} - , class: {disabled: soldOut(gift)} - }, [h('span.dollar', '$ '), format.centsToDollars(gift.amount_recurring), h('br'), h('small', 'Monthly') ]) - ]) - : '' // no recurring amount - ]) - ]) -} diff --git a/client/js/campaigns/show/gift-option-list.js b/client/js/campaigns/show/gift-option-list.js deleted file mode 100644 index a2ef43bb..00000000 --- a/client/js/campaigns/show/gift-option-list.js +++ /dev/null @@ -1,81 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const flyd = require('flyd') -const R = require('ramda') -const request = require('../../common/request') -const format = require('../../common/format') -const branding = require('../../components/nonprofit-branding') -flyd.mergeAll = require('flyd/module/mergeall') - -const quantityLeft = require('./gift-option-quantity-left') -const giftButton = require('./gift-option-button') - -// Pass in a stream that has a value when the gift options need to be refreshed, so we know when to refresh em! -function init(giftsNeedRefresh$, parentState) { - var state = { - timeRemaining$: parentState.timeRemaining$ - , clickOption$: flyd.stream() - , openEditGiftModal$: flyd.stream() - } - - // XXX some legacy viewscript mixed in here - flyd.map(gift => { - appl.open_modal('giftOptionFormModal') - appl.def('gift_options', {current: gift, is_updating: true}) - appl.def('gift_option_action', 'Edit') - }, state.openEditGiftModal$) - - const pageloadGifts$ = index() - const refreshedGifts$ = flyd.flatMap(index, giftsNeedRefresh$) - state.giftOptions$ = flyd.mergeAll([ - pageloadGifts$ - , refreshedGifts$ - , flyd.stream([]) // default before ajax loads - ]) - return state -} - -function index() { - const path = `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign_id}/campaign_gift_options` - return flyd.map( - req => req.body.data - , request({path, method: 'get'}).load - ) -} - -function view(state) { - return h('aside.sideGifts.u-marginBottom--15', { - class: {'u-hide': !state.giftOptions$().length} - }, R.map(giftBox(state), state.giftOptions$()) - ) -} - -const giftBox = state => gift => { - return h('section.u-relative', [ - h('div.sideGift.pastelBox--grey--dark', [ - h('h5.u-marginTop--0', gift.name) - , totalContributions(gift) - , quantityLeft(gift) - , h('p.u-marginBottom--15', gift.description) - , h('div', [ giftButton(state, gift) ]) - ]) - , (app.current_campaign_editor && app.is_parent_campaign) // Show edit button only if the current user is a parent campaign editor - ? h('button.button--tiny.absolute.edit.hasShadow', { - on: {click: ev => state.openEditGiftModal$(gift)} - }, [ - h('i.fa.fa-pencil') - , ' Edit Gift' - ]) - : '' // do not show gift edit button - ]) -} - -const totalContributions = gift => { - if(gift.hide_contributions) return '' - return h('p', [ - h('i.fa.fa-star', { style: { color: branding.base} }) - , ` ${format.numberWithCommas(gift.total_gifts)} Contribution${gift.total_gifts === 1 ? '' : 's'}` - ]) -} - -module.exports = {view, init} diff --git a/client/js/campaigns/show/gift-option-quantity-left.js b/client/js/campaigns/show/gift-option-quantity-left.js deleted file mode 100644 index e109f3ae..00000000 --- a/client/js/campaigns/show/gift-option-quantity-left.js +++ /dev/null @@ -1,18 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const soldOut = require('./is-sold-out') - -module.exports = gift => { - if(gift.hide_contributions || !gift.quantity) return '' - - if(soldOut(gift)) { - return h('p', [ - h('small.strong.highlight--white--small', 'SOLD OUT') - ]) - } else { - return h('p', [ - h('small.strong.highlight--white--small', [ `${gift.quantity - gift.total_gifts} Left` ]) - ]) - } -} - diff --git a/client/js/campaigns/show/is-sold-out.js b/client/js/campaigns/show/is-sold-out.js deleted file mode 100644 index e013f44b..00000000 --- a/client/js/campaigns/show/is-sold-out.js +++ /dev/null @@ -1,3 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = g => g.quantity && (g.quantity - g.total_gifts <= 0) - diff --git a/client/js/campaigns/show/metrics-and-contribute-box.js b/client/js/campaigns/show/metrics-and-contribute-box.js deleted file mode 100644 index ac031fef..00000000 --- a/client/js/campaigns/show/metrics-and-contribute-box.js +++ /dev/null @@ -1,105 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const flyd = require('flyd') -const h = require('snabbdom/h') -const format = require('../../common/format') -const branding = require('../../components/nonprofit-branding') - -// This is the box currently at the top right that shows some big metrics for -// the campaign, a big Contribute button (if enabled to show), days remaining -// (and a "campaign is done" message if no days remaining) - -function init(parentState) { - var state = { - clickContribute$: flyd.stream() - , timeRemaining$: parentState.timeRemaining$ - , metrics$: parentState.metrics$ - , loading$: parentState.loadingMetrics$ - } - - return state -} - - -function view(state) { - return h('div.pastelBox--grey--dark.u-relative.u-marginBottom--15.u-padding--15', [ - metrics(state) - , endedMessage(state) - , progressBar(state) - , contributeButton(state) - ]) -} - -const metrics = state => { - return h('div.campaignMetrics', [ - totalSupporters(state) - , totalRaised(state) - , daysLeft(state) - ]) -} - -const totalSupporters = state => { - if(!app.campaign.show_total_count) return '' - return h('div', [ - h('h4', [ - state.loading$() ? h('i.fa.fa-spin.fa-spinner') : format.numberWithCommas(state.metrics$().supporters_count) - ]) - , h('p', 'supporters') - ]) -} - -const totalRaised = state => { - if(!app.campaign.show_total_raised) return '' - return h('div', [ - h('h4', [ - state.loading$() ? h('i.fa.fa-spin.fa-spinner') : '$' + format.centsToDollars(state.metrics$().total_raised, {noCents: true}) - ]) - , h('p', [ - 'raised' - , app.campaign.hide_goal - ? '' - : ' of $' + format.centsToDollars(app.campaign.goal_amount) + ' goal' - ]) - ]) -} - -const daysLeft = state => { - if(!state.timeRemaining$()) return '' - return h('div', [ - h('h4', state.timeRemaining$()) - , h('p', 'remaining') - ]) -} - -const endedMessage = state => { - if(state.timeRemaining$()) return '' - return h('p', [ - `This campaign has ended, but you can still contribute by clicking the button below.` - ]) -} - -const progressBar = state => { - if(app.campaign.hide_thermometer) return '' - return h('div.progressBar--medium.u-marginBottom--15', [ - h('div.progressBar--medium-fill', { - style: { - width: R.clamp(1,100, format.percent( - state.metrics$().goal_amount - , state.metrics$().total_raised - ) + '%') - , 'background-color': branding.light - , transition: 'width 1s' - } - }) - ]) -} - -const contributeButton = state => { - return h('a.js-contributeButton.button--jumbo.u-width--full', { - style: {'background-color': branding.base} - , on: {click: state.clickContribute$} - }, [ 'Contribute' ]) -} - -module.exports = { init, view } - diff --git a/client/js/campaigns/show/page.js b/client/js/campaigns/show/page.js deleted file mode 100755 index e965c5ed..00000000 --- a/client/js/campaigns/show/page.js +++ /dev/null @@ -1,169 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const h = require('snabbdom/h') -const R = require('ramda') -const donateWiz = require('../../nonprofits/donate/wizard') -const render = require('ff-core/render') -const snabbdom = require('snabbdom') -const modal = require('ff-core/modal') -flyd.mergeAll = require('flyd/module/mergeall') -flyd.scanMerge = require('flyd/module/scanmerge') -const format = require('../../common/format') -const giftOptions = require('./gift-option-list') -const chooseGiftOptionsModal = require('./choose-gift-options-modal') -const metricsAndContributeBox = require('./metrics-and-contribute-box') -const timeRemaining = require('../../common/time-remaining') -const request = require('../../common/request') - -const activities = require('../../components/public-activities') - - -// Viewscript legacy side effect stuff -require('../../components/branded_fundraising') -require('../../common/on-change-sanitize-slug') -require('../../common/fundraiser_metrics') -require('../../components/fundraising/add_header_image') -require('../../common/restful_resource') -require('../../gift_options/index') -const on_ios11 = require('../../common/on-ios11') -const noScroll = require('no-scroll') -appl.ajax_gift_options.index() - - - -// Campaign editor only functionality -if(app.current_campaign_editor) { - require('./admin') - appl.def('current_campaign_editor', true) - require('../../gift_options/admin') - var create_info_card = require('../../supporters/info-card.es6') -} - -// Initialize the state for the top-level campaign component -// This includes the metrics, contribute button, gift options listing, and the donate wizard (most of right sidebar) -// Later can include the other viewscript pieces -function init() { - var state = { - timeRemaining$: timeRemaining(app.end_date_time, app.timezone), - } - - console.error(window.navigator.userAgent) - state.giftOptions = giftOptions.init(flyd.stream(), state) - - const metricsResp$ = flyd.map(R.prop('body'), request({ - method: 'get' - , path: `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign.id}/metrics` - }).load) - state.loadingMetrics$ = flyd.mergeAll([ - flyd.map(_ => false, metricsResp$) - , flyd.stream(true) - ]) - state.metrics$ = flyd.merge( - flyd.stream({goal_amount: 0, total_raised: 0, supporters_count: 0}) - , metricsResp$ - ) - state.metrics = metricsAndContributeBox.init(state) - - state.activities = activities.init('campaign', `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign_id}/activities`) - - - const contributeModalType$ = R.compose( - flyd.map(_ => - state.timeRemaining$() && state.giftOptions.giftOptions$().length - ? 'gifts' : 'regular') - )(state.metrics.clickContribute$) - - const clickContributeGifts$ = flyd.filter(x => x === 'gifts', contributeModalType$) - - const clickContributeRegular$ = flyd.filter(x => x === 'regular', contributeModalType$) - - state.clickRegularContribution$ = flyd.stream() - - const startWiz$ = flyd.mergeAll([ - state.giftOptions.clickOption$ - , clickContributeRegular$ - , state.clickRegularContribution$ - ]) - - state.selectedModalGift$ = flyd.stream({}) - - state.modalID$ = flyd.merge( - flyd.map(R.always('chooseGiftOptionsModal'), clickContributeGifts$) - , flyd.map(R.always('donationModal'), startWiz$)) - - flyd.on((id) => { - if (on_ios11() && id === null) { - noScroll.off() - } - }, state.modalID$) - - flyd.on((id) => { - if (on_ios11() && id !== null) { - noScroll.on() - } - }, state.modalID$) - - // Stream of which gift option you have selected - const giftOption$ = flyd.map(setGiftParams, state.giftOptions.clickOption$) - const donateParam$ = flyd.scanMerge([ - [state.metrics.clickContribute$, resetDonateForm] - , [giftOption$, setGiftOption] - ], {campaign_id: app.campaign.id} ) - - state.donateWiz = donateWiz.init(donateParam$) - - return state -} - -const resetDonateForm = (params, _) => R.merge(params, { - single_amount: undefined -, gift_option: undefined -, type: undefined -}) - -const setGiftOption = (params, gift) => R.merge(params, { - single_amount: gift.amount / 100 -, gift_option: gift -, type: gift.type -}) - - -// Set the donate wizard parameters using data from a gift option -const setGiftParams = (triple) => { - var [gift, amount, type] = triple - return { amount: amount, type: type , id: gift.id, name: gift.name, to_ship: gift.to_ship} -} - -function view(state) { - return h('div', [ - metricsAndContributeBox.view(state.metrics) - , giftOptions.view(state.giftOptions) - , activities.view(state.activities) - , h('div.donationModal', [ - modal({ - thisID: 'donationModal' - , id$: state.modalID$ - , body: donateWiz.view(state.donateWiz) - // , notCloseable: state.donateWiz.paymentStep.cardForm.loading$() - }) - , modal({ - thisID: 'chooseGiftOptionsModal' - , title: 'Contribute' - , id$: state.modalID$ - , body: chooseGiftOptionsModal(state) - }) - ]) - ]) -} - -// -- Render to the page - -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) - -render({state: init(), view, patch, container: document.querySelector('.ff-sidebar')}) - diff --git a/client/js/campaigns/show/tour.js b/client/js/campaigns/show/tour.js deleted file mode 100644 index 161a3174..00000000 --- a/client/js/campaigns/show/tour.js +++ /dev/null @@ -1,35 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/vendor/bootstrap-tour-standalone') - -var tour_campaign = new Tour({ - steps: [ - { - orphan: true, - title: 'Welcome to your new campaign!', - content: "Click 'Next' to find out how you can flesh out your campaign before sharing it." - }, - { - title: 'Manage your campaign', - placement: 'bottom', - element: '.tour-admin', - content: "You can manage your campaign by clicking on these buttons at the top of the page." - }, - { - element: '.froala-box', - title: 'Write your story', - content: "Every successful campaign has a powerful story. Write and edit your story in the area to the left. You can add formatting by clicking the icons at the top of this box." - }, - { - orphan: true, - title: 'You’re on your way!', - content: "Once you’ve written your campaign story and added gift options, you can start sharing it with all your contacts. We’re excited for it to succeed!" - } - ] -}) - -if($.cookie('tour_campaign') === String(app.nonprofit_id)) { - $.removeCookie('tour_campaign', {path: '/'}) - tour_campaign.init() - tour_campaign.restart() -} - diff --git a/client/js/campaigns/supporters/index/index.es6 b/client/js/campaigns/supporters/index/index.es6 deleted file mode 100644 index 941ecbc8..00000000 --- a/client/js/campaigns/supporters/index/index.es6 +++ /dev/null @@ -1,63 +0,0 @@ -// License: LGPL-3.0-or-later -const request = require('../../../common/super-agent-frp') -const view = require('vvvview') -const flyd = require('flyd') -const scanMerge = require('flyd/module/scanmerge') -const flatMap = require('flyd/module/flatmap') -const Im = require('immutable') -const Map = Im.Map -const fromJS = Im.fromJS - -const list = require('./supporter-list.es6') - -var el = document.querySelector('.js-view-supporters') -var state = Map({loading: true}) -var listView = view(list.root, el, state) - -// Given a query object, return an ajax stream -const request_index = query => - request - .get(`/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/supporters`) - .query(query) - .perform() - -var $searchResponses = flatMap(request_index, list.$streams.searches) - -const appendPage = (state, resp) => { - var oldSupporters = state.getIn(['supporters', 'data']) - var newData = fromJS(resp.body) - if(oldSupporters) newData = newData.set('data', oldSupporters.concat(newData.get('data'))) - return state - .set('supporters', newData) - .set('moreLoading', false) - .set('loading', false) -} - -const $showMorePages = flyd.scan( - count => count + 1 - , 1 - , list.$streams.showMore) - -const $newPages = flatMap( - page => request_index({page: page}) - , $showMorePages) - -const setResults = (state, resp) => - state.set('supporters', fromJS(resp.body)).set('loading', false) - -var $giftLevelResponses = - request.get(`/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/campaign_gift_options`).perform() - -var $state = flyd.immediate(scanMerge([ - [list.$streams.searches, state => state.set('loading', true).set('isSearching', true)], - [list.$streams.showMore, state => state.set('moreLoading', true).set('page', state.get('page') + 1)], - [$newPages, appendPage], - [$searchResponses, setResults], - [$giftLevelResponses, (state, resp) => state.set('gift_levels', fromJS(resp.body))], -], state)) - -window.$state =$state -window.$giftLevelResponses= $giftLevelResponses - -flyd.map(listView, $state) - diff --git a/client/js/campaigns/supporters/index/meta.es6 b/client/js/campaigns/supporters/index/meta.es6 deleted file mode 100644 index 3d071607..00000000 --- a/client/js/campaigns/supporters/index/meta.es6 +++ /dev/null @@ -1,27 +0,0 @@ -// License: LGPL-3.0-or-later -// Table meta for the supporter listing under Campaigns -const h = require('virtual-dom/h') -const thunk = require('vdom-thunk') -const search = require('../../../components/tables/search.es6') - -const root = state => - h('div.container', [ - thunk(search.root, state), - h('a.table-meta-button.white', { - href: `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/admin/donations.csv`, - target: '_blank', - }, [ h('i.fa.fa-file-text'), ' Export ' ]), - /* - h('a.table-meta-button.green', { - onclick: $.showEmailModal - }, [ h('i.fa.fa-envelope'), ' Email ' ]) - */ - ]) - -module.exports = { - root: root, - $streams: { - searches: search.$streams.searches - } -} - diff --git a/client/js/campaigns/supporters/index/metrics.es6 b/client/js/campaigns/supporters/index/metrics.es6 deleted file mode 100644 index a476ba58..00000000 --- a/client/js/campaigns/supporters/index/metrics.es6 +++ /dev/null @@ -1,35 +0,0 @@ -// License: LGPL-3.0-or-later -const view = require('vvvview') -const h = require('virtual-dom/h') -const flyd = require('flyd') -const scanMerge = require('flyd/module/scanmerge') -const thunk = require('vdom-thunk') -const request = require('../../../common/super-agent-frp') -const format = require('../../../common/format') -const Im = require('immutable') -const Map = Im.Map -const fromJS = Im.fromJS - -const root = state => { - if(!state || !state.get('data')) return h('span') - return h('table.table--plaid', [ - h('thead', [ - h('tr', [h('th', 'Gift option'), h('th', 'Count'), h('th', 'One time'), h('th', 'Recurring')]), - ]), - h('tbody', state.get('data').map(gift => thunk(giftRow, gift)).toJS()) - ]) -} - -const giftRow = gift => { - - var name = gift.get('name') - name = !name || !name.length ? 'No Gift Option Chosen' : name - return h('tr', [ - h('td', h('strong', name)), - h('td', (gift.get('total_donations') || 0) + ''), - h('td', '$' + utils.cents_to_dollars(gift.get('total_one_time'))), - h('td', '$' + utils.cents_to_dollars(gift.get('total_recurring'))) - ]) -} - -module.exports = { root: root, $streams: $ } diff --git a/client/js/campaigns/supporters/index/page.js b/client/js/campaigns/supporters/index/page.js deleted file mode 100644 index c1d016b8..00000000 --- a/client/js/campaigns/supporters/index/page.js +++ /dev/null @@ -1,5 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../timeline') -require('../../totals') -require('./index.es6') - diff --git a/client/js/campaigns/supporters/index/supporter-list.es6 b/client/js/campaigns/supporters/index/supporter-list.es6 deleted file mode 100644 index b5e64be4..00000000 --- a/client/js/campaigns/supporters/index/supporter-list.es6 +++ /dev/null @@ -1,29 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('virtual-dom/h') -const thunk = require('vdom-thunk') -const flyd = require('flyd') - - -const metrics = require('./metrics.es6') -const meta = require('./meta.es6') -const supporterTable = require('./supporter-table.es6') - -var $ = { - showMore: supporterTable.$streams.showMore, - searches: meta.$streams.searches, - showEmailModal: flyd.stream(), -} - -const root = state => - h('div', [ - h('section.table-meta', thunk(meta.root, state)), - h('section.metrics.container', thunk(metrics.root, state.get('gift_levels'))), - h('hr'), - h('section.container', thunk(supporterTable.root, state)), - h('hr'), - ]) - -// Table meta for the supporter listing under Campaigns - -module.exports = {root: root, $streams: $} - diff --git a/client/js/campaigns/supporters/index/supporter-table.es6 b/client/js/campaigns/supporters/index/supporter-table.es6 deleted file mode 100644 index b5c053b1..00000000 --- a/client/js/campaigns/supporters/index/supporter-table.es6 +++ /dev/null @@ -1,69 +0,0 @@ -// License: LGPL-3.0-or-later -const thunk = require('vdom-thunk') -const h = require('virtual-dom/h') -const flyd = require('flyd') -const showMoreBtn = require('../../../components/show-more-button.es6') -const format = require("../../../common/format") -const date = format.date -const sql = format.sql - -const root = state => { - console.log({state}) - var supporters = state.get('supporters') - if(state.get('loading')) { - return h('p.noResults', ' Loading...') - } else if(supporters.get('data').count()) { - return h('div', [ - h('table.table--plaid', [ - h('thead', [ - h('th', 'Name'), - h('th', 'Total'), - h('th', 'Gift options'), - h('th', 'Latest gift'), - h('th', 'Campaign creator') - ]), - thunk(trs, supporters.get('data')), - ]), - thunk(showMoreBtn.root, state.get('moreLoading'), supporters.get('remaining')) - ]) - } else if (state.get('isSearching')) { - return h('p.noResults', ["Supporter not found."]) - } else { - return h('p.noResults', ["No donors yet. ", h('a', {href: './'}, 'Return to the campaign page.')]) - } -} - -const trs = supporters => - h('tbody', supporters.map(supp => thunk(supporterRow, supp)).toJS()) - -const supporterRow = supporter => - h('tr', [ - h('td', - h('a' - , { - href: `/nonprofits/${app.nonprofit_id}/supporters?sid=${supporter.get('id')}` - , target: '_blank' - } - , [supporter.get('name') - , h('br') - , h('small', supporter.get('email')) - ] - ) - ) - , h('td', '$' + utils.cents_to_dollars(supporter.get('total_raised'))), - h('td', supporter.get('campaign_gift_names').toJS().join(', ')), - h('td', supporter.get('latest_gift')), - h('td', {}, supporter.get('campaign_creator_emails').toJS().map( - function(i, index, array) { - return h('a', {href: `mailto:${i}`}, - i + ((i < (array.length - 1)) ? ", " : "")) - })), - ]) - -module.exports = { - root: root, - $streams: { - showMore: showMoreBtn.$streams.nextPageClicks, - } -} - diff --git a/client/js/campaigns/timeline.js b/client/js/campaigns/timeline.js deleted file mode 100644 index 5ff8c455..00000000 --- a/client/js/campaigns/timeline.js +++ /dev/null @@ -1,93 +0,0 @@ -// License: LGPL-3.0-or-later -const request = require('../common/client') -const R = require('ramda') -const Chart = require('chart.js') -const moment = require('moment') -const dateRange = require('../components/date-range') -const chartOptions = require('../components/chart-options') - -var url = `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/timeline` - -function query() { - appl.def('loading_chart', true) - request.get(url) - .end(function(err, resp) { - appl.def('loading_chart', false) - var ctx = document.getElementById('js-timeline').getContext('2d') - new Chart(ctx, { - type: 'line' - , data: formatData(cumulative(resp.body)) - , options: chartOptions.dollars - }) - }) -} - -function cumulative(data) { - var moments = dateRange(R.head(data).date, R.last(data).date, 'days') - var dateStrings = R.map((m) => m.format('YYYY-MM-DD'), moments) - - var proto = { - offsite_cents: 0 - , onetime_cents: 0 - , recurring_cents: 0 - , total_cents: 0 - } - - var dateDictionary = R.reduce((a,b) => { - a[b] = R.merge(proto, {date: b}) - return a - }, {}, dateStrings) - - R.reduce((a, b) => { - a[b.date] = b - return a - }, dateDictionary, data) - - return R.tail(R.reduce((a, b) => { - var last = R.last(a) - b.offsite_cents += last.offsite_cents - b.onetime_cents += last.onetime_cents - b.recurring_cents += last.recurring_cents - b.total_cents += last.total_cents - return R.append(b, a) - }, [proto], R.values(dateDictionary))) -} - -function formatData(data) { - return { - labels: R.map((st) => moment(st).format('M/D/YYYY'), R.pluck('date', data)) - , datasets: [ - dataset('Total' - , 'total_cents' - , '190, 190, 190' - , data) - , dataset('One time' - , 'onetime_cents' - , '66, 179, 223' - , data) - , dataset('Recurring' - , 'recurring_cents' - , '240, 205, 108' - , data) - , dataset('Offsite' - , 'offsite_cents' - , '95, 184, 141' - , data) - ] - } -} - -function dataset(label, key, rgb, data) { - return { - label: label - , data: R.pluck(key, data) - , borderColor: `rgb(${rgb})` - , backgroundColor: `rgba(${rgb},0.2)` - , fill: false - , pointRadius: 0 - , pointHitRadius: 2 - } -} - -query() - diff --git a/client/js/campaigns/totals.js b/client/js/campaigns/totals.js deleted file mode 100644 index 0bcb1e02..00000000 --- a/client/js/campaigns/totals.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -const request = require('../common/request') -const flyd = require('flyd') -const R = require('ramda') -var path = `/nonprofits/${app.nonprofit_id}/campaigns/${ENV.campaignID}/totals` - -const resp$ = flyd.map(R.prop('body'), request({path, method: 'GET'}).load) - -appl.def('loading_totals', true) - -flyd.map(response => { - appl.def('loading_totals', false) - appl.def('campaign_totals', response) -}, resp$) - diff --git a/client/js/cards/create-frp.es6 b/client/js/cards/create-frp.es6 deleted file mode 100644 index 94ff4e9e..00000000 --- a/client/js/cards/create-frp.es6 +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const flyd_flatMap = require('flyd/module/flatmap') - -// Given an object of card data, return a stream of stripe tokenization responses -module.exports = obj => { - var $ = flyd.stream() - Stripe.card.createToken(obj, (status, resp) => $(resp)) - return $ -} - diff --git a/client/js/cards/create.js b/client/js/cards/create.js deleted file mode 100644 index c8c22285..00000000 --- a/client/js/cards/create.js +++ /dev/null @@ -1,129 +0,0 @@ -// License: LGPL-3.0-or-later -// Include the cards/fields partial to use with this. -// Call appl.card_form.create(card_obj) to start the card creation process. -// Use the appl.card_form.on_fail callback to handle failures. -// Use the appl.card_form.on_complete callback to handle completion. -// This does not create any donations -- do the donation creation inside appl.card_form.on_complete. - -// Not namespacing card_form; only show one card form on the page at any time - -var request = require('../common/super-agent-promise') -var format_err = require('../common/format_response_error') - -module.exports = create_card - -// UI state defaults -appl.def('card_form', { - loading: false, - error: false, - status: '', - on_complete: function() {}, - on_fail: function() {}, - progress_width: '0%' // Width of the progress bar -}) - -// Define some status messages and progress bar widths for each step of the process -var statuses = { - before_tokenization: { - progress_width: '20%', - status: 'Double-checking your card...' - }, - before_create: { - progress_width: '75%', - status: 'Looks good! Sending the carrier pigeons...' - }, - on_complete: { - progress_width: '100%', - status: 'Processing payment...' - } -} - -// Tokenize with stripe, then save to our db. -// The first argument must be a holder object that has 'type' and 'id' keys. -// eg: {holder: {type: 'Nonprofit', id: 1}} -function create_card(holder, card_obj, options) { - options = options || {} - if(appl.card_form.loading) return - appl.def('card_form', { loading: true, error: false }) - appl.def('card_form', statuses.before_tokenization) - - // Delete the cvc key from card_obj if - // the value of cvc is a blank string. - // Otherwise, Stripe will return an error for - // incorrect security code. - if(card_obj.cvc === '') { - delete card_obj['cvc'] - } - - // First, tokenize the card with Stripe.js - return tokenize_with_stripe(card_obj) - .catch(display_stripe_err) - // Then, save a Card record in our db - .then(function(stripe_resp) { - appl.def('card_form', statuses.before_create) - return create_record(holder, stripe_resp, options) - }) - .then(function(resp) { - appl.def('card_form', statuses.on_complete) - return resp.body - }) - .catch(display_err) -} - -// Post to stripe to get back a stripe_card_token -function tokenize_with_stripe(card_obj) { - return new Promise(function(resolve, reject) { - Stripe.card.createToken(card_obj, function(status, resp) { - if(resp.error) reject(resp) - else resolve(resp) - }) - }) -} - -// Save a record of the card in our own db -function create_record(holder, stripe_resp, options={}) { - var output = {card: { - holder_type: holder.type, - holder_id: holder.id, - email: holder.email, - cardholders_name: stripe_resp.name, - name: stripe_resp.card.brand + ' *' + stripe_resp.card.last4, - stripe_card_token: stripe_resp.id, - stripe_card_id: stripe_resp.card.id - }} - if (options['event_id']) - { - output['event_id'] = options['event_id'] - } - - return request.post(options.path || '/cards') - .send(output) - .perform() -} - -// Set UI state to display an error in the card form. -function display_err(resp) { - if(resp && resp.body) { - appl.def('card_form', { - loading: false, - error: true, - status: format_err(resp), - progress_width: '0%' - }) - appl.def('loading', false) - } -} - -function display_stripe_err(resp) { - if(resp && resp.error) { - appl.def('card_form', { - loading: false, - error: true, - status: resp.error.message, - progress_width: '0%' - }) - appl.def('loading', false) - - throw new Error() - } -} diff --git a/client/js/common/ajax/check_campaign_or_event_name.js b/client/js/common/ajax/check_campaign_or_event_name.js deleted file mode 100644 index 517e9ee7..00000000 --- a/client/js/common/ajax/check_campaign_or_event_name.js +++ /dev/null @@ -1,16 +0,0 @@ -// License: LGPL-3.0-or-later -var R = require('ramda') -var request = require('../client') - -module.exports = function(name, event_or_campaign, callback) { - request.get(`/nonprofits/${app.nonprofit_id}/${event_or_campaign}s/name_and_id`) - .end(function(err, resp){ - var names = resp.body.map(x => x.name) - if(R.contains(name, names)) { - appl.notify(`Oops. It looks like you already have ${event_or_campaign === 'campaign' ? 'a' : 'an'} ${event_or_campaign} named '${name}'. Please choose a different name and try again.`) - return - } - callback() - }) -} - diff --git a/client/js/common/ajax/get_campaign_and_event_names_and_ids.js b/client/js/common/ajax/get_campaign_and_event_names_and_ids.js deleted file mode 100644 index 842d53ba..00000000 --- a/client/js/common/ajax/get_campaign_and_event_names_and_ids.js +++ /dev/null @@ -1,36 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../client') - -module.exports = function(npo_id) { - var campaignsPath = '/nonprofits/' + npo_id + '/campaigns/name_and_id' - var eventsPath = '/nonprofits/' + npo_id + '/events/name_and_id' - - request.get(campaignsPath).end(function(err, resp){ - var dataResponse = [] - - if (!err) { - resp.body.unshift(false) - dataResponse = resp.body.map((i) => { - if (i.isChildCampaign) - { - return {id: i.id, name: i.name + " - " + i.creator} - } - else - { - return {id: i.id, name: i.name} - } - }) - } - appl.def('campaigns.data', dataResponse) - }) - - request.get(eventsPath).end(function(err, resp){ - var dataResponse = [] - if(!err) { - resp.body.unshift(false) - dataResponse = resp.body - } - - appl.def('events.data', dataResponse) - }) -} diff --git a/client/js/common/application_view.js b/client/js/common/application_view.js deleted file mode 100644 index a60cbdd9..00000000 --- a/client/js/common/application_view.js +++ /dev/null @@ -1,393 +0,0 @@ -// License: LGPL-3.0-or-later -var confirmation = require('./confirmation') -var notification = require('./notification') -var request = require("superagent") -var moment = require('moment-timezone') -var client = require('./client') -var appl = require('view-script') -const on_ios11 = require('./on-ios11') -const noScroll = require('no-scroll') - -module.exports = appl - -// A couple short convenience functions for disabling/enabling the global -// loading state -appl.is_loading = function() {appl.def('loading', true)} -appl.not_loading = function() {appl.def('loading', false)} -appl.not_loading() -// Open a modal given by its modal id (uses the modal div's 'id' attribute) -appl.def('open_modal', function(modalId) { - $('.modal').removeClass('inView') - - //$('body').scrollTop(0) - $('#' + modalId).addClass('inView') - - $('body').addClass('is-showingModal') - if (on_ios11()){ - noScroll.on() - } - return appl -}) - -// Close any and all open modals -appl.def('close_modal', function() { - $('.modal').removeClass('inView') - $('body').removeClass('is-showingModal') - if (on_ios11()) { - noScroll.off() - } - return appl -}) - -// Open a given modal id only when the User's Account is confirmed via email -// If the user's account is not confirmed, then show an informational modal -// about confirming their account -appl.def('open_modal_if_confirmed', function(modalId){ - if (app.user && app.user.confirmed) - appl.open_modal(modalId) - else if (app.user && !app.user.confirmed) - appl.open_modal('emailConfirmationModal') - else - appl.open_modal('signUpModal') - return appl -}) - - -// Open a confirmation modal for the user to click 'yes' or 'no' -// Optionally pass in a string message as the first arg (default is 'Are you sure?') -// The last argument is the function to execute when 'yes' is clicked -// Clicking 'no' simply closes the modal -appl.def_lazy('confirm', function() { - var msg, expr, node, self = this - if(arguments.length === 2) { - msg = 'Are you sure?' - expr = arguments[0] - node = arguments[1] - } else { - msg = appl.vs(arguments[0]) - expr = arguments[1] - node = arguments[2] - } - - var result = confirmation(msg) - result.confirmed = function() { appl.vs(expr, node) } - return self -}) - - -// Display a temporary notification message at the bottom of the window -appl.def('notify', function(msg) { - notification(msg) - return appl -}) - -// Convert cents to dollars -appl.def('cents_to_dollars', function(cents) { - return utils.cents_to_dollars(cents) -}) - - -const momentTz = date => - moment.tz(date, "YYYY-MM-DD HH:mm:ss", 'UTC').tz(ENV.nonprofitTimezone || 'UTC') - -// Return a date in the format MM/DD/YY for a given date string or moment obj -appl.def('readable_date', function(date) { - if(!date) return - return momentTz(date).format("MM/DD/YY") -}) - -// Given a created_at string (eg. Charge.last.created_at.to_s), convert it to a readable date-time string -appl.def('readable_date_time', function(date) { - if(!date) return - return momentTz(date).format("MM/DD/YY H:mma z") -}) - -// converts the return value of readable_date_time to it's ISO equivalent -appl.def('readable_date_time_to_iso', date => { - if(!date) return - return moment.tz(date, 'MM/DD/YY H:mma z', ENV.nonprofitTimezone || 'UTC') - .tz('UTC') - .toISOString() -}) - -// Get the month number (eg 01,02...) for the given date string (or moment obj) -appl.def('get_month', function(date) { - var monthNum = moment(date).month() - return moment().month(monthNum).format('MMM') -}) - -// Get the year (eg 2017) for the given date string (or moment obj) -appl.def('get_year', function(date) { - return moment(date).year() -}) - -// Get the day (number in the month) for the given date string (or moment obj) -appl.def('get_day', function(date) { - return moment(date).date() -}) - - -// Get the percentage of x over y -// eg: appl.percentage(34, 69) -> "49.28%" -appl.def('percentage', function(x, y) { - return String(x / y * 100) + '%' -}) - - -// Given a quantity and a plural word describing that quantity, -// return the proper version of that word for that quantitiy -// eg: appl.pluralize(4, 'tomatoes') -> "4 tomatoes" -// appl.pluralize(1, 'donors') -> "1 donor" -appl.def('pluralize', function(quantity, plural_word) { - var str = String(quantity) + ' ' - if(quantity !== 1) return str+plural_word - else return str + appl.to_singular(plural_word) -}) - - -// Convert (most) words from their plural to their singular form -// Works with simple s-endings, ies-endings, and oes-endings -appl.def('to_singular', function(plural_word) { - return plural_word - .replace(/ies$/, 'y') - .replace(/oes$/, 'o') - .replace(/s$/, '') -}) - - -// Truncate a text and add ellipsis to the end -appl.def('append_ellipsis', function(text, length) { - if(text.length <= length) return text - return text.slice(0,length).replace(/ [^ ]+$/, ' ...') -}) - - -// General viewscript utilities -// All of these are to be added to the actual viewscript package in the future - -// Push a given value into the arr given by the property name 'arr_key' -// Mutates the array stored at 'arr_key' -// appl.def('arr', [1,2,3]) -// appl.push('arr', 4) -// appl.arr == [1,2,3,4] -appl.def('push', function(val, arr_key, node) { - var arr = appl.vs(arr_key, node) - if(!arr || !arr.length) arr = [] - arr.push(val) - appl.def(arr_key, arr) - return appl -}) - - -// Concatenate two arrays (this is mutating) -// The first array is given by its property name and will be mutated -// The second array is the array itself to concatenate -// appl.def('arr1', [1,2,3]) -// appl.concat('arr1', [4,5,6]) -// appl.arr1 == [1,2,3,4,5,6] -appl.def('concat', function(arr1_key, arr2, node) { - var arr1 = appl.vs(arr1_key, node) - appl.def(arr1_key, arr1.concat(arr2)) - return appl -}) - - -// Merge all key/vals from set_obj into all objects in the array given by the property 'arr_key' -// eg: -// appl.def('arr_of_objs', [{id: 1, name: 'Bob'}, {id: 2, name: 'Holga'}] -// appl.update_all('arr_of_objs', {name: 'Morty'}) -// appl.arr_of_objs == [{id: 1, name: 'Morty'}, {id: 2, name: 'Morty'}] -appl.def('update_all', function(arr_key, set_obj, node) { - appl.def(arr_key, appl.vs(arr_key).map(function(obj) { - for(var key in set_obj) obj[key] = set_obj[key] - return obj - })) -}) - - -// Given an array of objects in the view state (with property name 'arr_key'), -// and given an object to match on ('obj_matcher'), -// and given an object with values to set ('set_obj'), -// then set each object that matches key/vals in the obj_matcher to the key/vals in set_obj -// -// eg, if val at arr_key is: [{id: 1, name: 'Bob'}, {id: 2, name: 'Holga'}] -// and obj_matcher is: {id: 1} -// and set_obj is: {name: 'Gertrude'} -// then result will be: [{id: 1, name: 'Gertrude'}, {id: 2, name: 'Holga'}] -appl.def('find_and_set', function(arr_key, obj_matcher, set_obj, node) { - var arr = appl.vs(arr_key) - if(!arr) return appl - var result = arr.map(function(obj) { - for (var key in obj_matcher) { - if(obj_matcher[key] === obj[key]) { - return utils.merge(obj, set_obj) - } - } - return obj - }) - appl.def(arr_key, result) - return appl -}) - -appl.def('find_and_remove', function(arr_key, obj_matcher, set_obj, node) { - var arr = appl.vs(arr_key) - if(!arr) return appl - var result = arr.reduce(function(new_arr, obj) { - for (var key in obj_matcher) { - if(obj_matcher[key] === obj[key]) { - return new_arr - } else { - new_arr.push(obj) - return new_arr - } - } - }, []) - appl.def(arr_key, result) - return appl -}) - - -// Return a boolean whether the parent input is checked (must be a type checkbox) -appl.def('is_checked', function(node) { - return appl.prev_elem(node).checked -}) - -// Check a parent input node (must be type checkbox) -appl.def('check', function(node) { - appl.prev_elem(node).checked = true -}) - -// Uncheck a parent input node (must be type checkbox) -appl.def('uncheck', function(node) { - appl.prev_elem(node).checked = false -}) - -// Check the parent node if the predicate is true -appl.def('checked_if', function(pred, node) { - if(pred) appl.prev_elem(node).checked = true - else appl.prev_elem(node).checked = false -}) - -// Remove an attribute from the parent node -appl.def('remove_attr', function(attr, node) { - appl.prev_elem(node).removeAttribute(attr) -}) - -appl.def('remove_attr_if', function(pred, attr, node) { - if(!node) return - var n = appl.prev_elem(node) - if(pred) { - if(!n.hasAttribute('data-attr-' + attr)) n.setAttribute('data-attr-' + attr, n.getAttribute(attr)) // cache attr to add back in - n.removeAttribute(attr) - } else if(!n.hasAttribute(attr)) { - var val = n.getAttribute('data-attr-' + attr) - n.setAttribute(attr, val) - } -}) - -// Map over the given list and update it in the view -appl.transform = function(name, fn) { - var result = appl.vs(name).map(fn) - appl.def(name, result) - return result -} - -// Return the current URL path -appl.def('pathname', function() { return window.location.pathname }) -// Return the root url -appl.def('root_url', function() { return window.location.origin }) - -// Trigger a property to get updated in the view -appl.def('trigger_update', function(prop) { - return appl.def(prop, appl.vs(prop)) -}) - - -appl.def('snake_case', function(string) { - return string.replace(/ /g,'_') -}) - -appl.def('sort_arr_of_objs_by_key', function(arr_of_objs, key) { - return arr_of_objs.sort(function(a, b) { - return a[key].localeCompare(b[key]); - }) -}) - -// Convert a positive integer into an ordinal (1st, 2nd, 3rd...) -appl.def('ordinalize', function(n) { - if(n <= 0) return n - // Deal with the preteen punks first - if([11,12,13].indexOf(n) !== -1) return String(n) + 'th' - var str = String(n) - var lst = str[str.length-1] - if(lst === '1') return String(n) + 'st' - else if(lst === '2') return String(n) + 'nd' - else if(lst === '3') return String(n) + 'rd' - else return String(n) + 'th' -}) - -appl.def('toggle_side_nav', function(){ - if(appl.side_nav_is_open) - appl.def('side_nav_is_open', false) - else - appl.def('side_nav_is_open', true) -}) - -appl.def('head', function(arr) { - if(arr === undefined) return undefined - return arr[0] -}) - -appl.def('select_drop_down', function(node) { - var $li = $(node).parent() - var $dropDown = $li.parents('.dropDown') - $dropDown.find('li').removeClass('is-selected') - $dropDown.find('.dropDown-toggle').removeClass('is-droppedDown') - $li.addClass('is-selected') -}) - -appl.def('clear_drop_down', function(node){ - var $dropDown = $(node).parents('.dropDown') - $dropDown.find('li').removeClass('is-selected') - $dropDown.find('.dropDown-toggle').removeClass('is-droppedDown') -}) - -appl.def('strip_tags', function(html){ - if(!html) return - return html.replace(/(<([^>]+)>)/ig," ") -}) - -appl.def('replace', function(string, matcher, replacer) { - if(!string) return - // the new RegExp constructor takes a string - // and returns a regex: new RegExp("a|b", "i") becomes /a|b/i - return string.replace(new RegExp(matcher, 'g'), replacer) -}) - -appl.def('number_with_commas', function(n){ - if(!n){return} - return utils.number_with_commas(n) -}) - -appl.def('remove_commas', function(s) { - return s.replace(/,/g, '') -}) - -appl.def('percentage', function(x, y, number_of_decimals){ - if(!x || !y) return 0 - number_of_decimals = number_of_decimals || 2 - return Number((y/x * 100).toFixed(number_of_decimals)) -}) - -appl.def('clean_url', function(string){ - return string.replace(/.*?:\/\//g, "") -}) - -appl.def('address_with_commas', function(address, city, state){ - return utils.address_with_commas(address, city, state) -}) - -appl.def('format_phone', function(st) { - return utils.pretty_phone(st) -}) - diff --git a/client/js/common/apply-pikaday.js b/client/js/common/apply-pikaday.js deleted file mode 100644 index c16cc20e..00000000 --- a/client/js/common/apply-pikaday.js +++ /dev/null @@ -1,14 +0,0 @@ -// License: LGPL-3.0-or-later -const bind = require('attr-binder') -const Pikaday = require('pikaday') -const moment = require('moment') - -bind('apply-pikaday', function(field, format) { - const setDefaultDate = field.getAttribute('pikaday-setDefaultDate') - const maxDate_str = field.getAttribute('pikaday-maxDate') - const maxDate = maxDate_str ? moment(maxDate_str) : undefined - const defaultDate_str = field.getAttribute('pikaday-defaultDate') - const defaultDate = defaultDate_str ? moment(defaultDate_str) : undefined - new Pikaday({format, setDefaultDate, field, maxDate, defaultDate}) -}) - diff --git a/client/js/common/autosubmit.js b/client/js/common/autosubmit.js deleted file mode 100644 index d0425446..00000000 --- a/client/js/common/autosubmit.js +++ /dev/null @@ -1,72 +0,0 @@ -// License: LGPL-3.0-or-later -var confirmation = require('./confirmation') -var notification = require('./notification') - -$('form[autosubmit]').submit(function(e) { - var self = this - e.preventDefault() - - if(this.hasAttribute('data-confirm')) { - var result = confirmation() - result.confirmed = function() { - submit_form(e.currentTarget) - } - } else submit_form(e.currentTarget) -}) - -function submit_form(form_el, on_success) { - var path = form_el.getAttribute('action') - var method = form_el.getAttribute('method') - var form_data = new FormData(form_el) - $(form_el).find('button[type="submit"]').loading() - $(form_el).find('.error').text('') - - var notice = form_el.getAttribute('notice') - if(notice) $.cookie('notice', notice, {path: '/'}) - - $.ajax({ - type: method, - url: path, - data: form_data, - dataType: 'json', - processData: false, - contentType: false - }) - .done(function(d) { - if(form_el.hasAttribute('data-reload-with-slug')) - window.location = d['url'] - else if(form_el.hasAttribute('data-reload')) - window.location.reload() - else if(form_el.hasAttribute('data-redirect')) { - var redirect = form_el.getAttribute('data-redirect') - if(redirect) window.location.href = redirect - else if(d.url) window.location.href = d.url - } else { - var msg = form_el.getAttribute('data-success-message') - if(msg) notification(msg) - $(form_el).find('button[type="submit"]').disableLoading() - } - if(on_success) on_success(d) - }) - .fail(function(d) { - $(form_el).find('.error').text(utils.print_error(d)) - $(form_el).find('button[type="submit"]').disableLoading() - }) -} - -// Third closure - -appl.def_lazy('autosubmit', function(callback, node) { - if(!node || !node.parentNode) return - - var self = this, parent = node.parentNode - - parent.onsubmit = function(ev) { - ev.preventDefault() - - if(parent.hasAttribute('data-confirm')) - confirmation().confirmed = function() { submit_form(parent, function() {appl.vs(callback)}) } - else submit_form(parent, function() { appl.vs(callback) }) - } -}) - diff --git a/client/js/common/brand-fonts.js b/client/js/common/brand-fonts.js deleted file mode 100644 index 47c8fd93..00000000 --- a/client/js/common/brand-fonts.js +++ /dev/null @@ -1,8 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = { - helvetica: {family: "'Helvetica Neue', Helvetica, Arial, sans-serif", name: 'Helvetica'}, - futura: {family: "'Futura', Arial, sans-serif", name: 'Futura'}, - open: {family: "Open Sans, 'Helvetica Neue', Arial, sans-serif", name: 'Open Sans'}, - georgia: {family: "Georgia, serif", name: 'Georgia'}, - bitter: {family: "'Bitter', serif", name: 'Bitter'} -} diff --git a/client/js/common/class-object.js b/client/js/common/class-object.js deleted file mode 100644 index 7bdd32cd..00000000 --- a/client/js/common/class-object.js +++ /dev/null @@ -1,8 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') - -module.exports = (classes='') => R.reduce( - (a, b) => {a[b] = true; return a} - , {} - , R.drop(1, classes.split('.'))) - diff --git a/client/js/common/client.js b/client/js/common/client.js deleted file mode 100644 index e824e804..00000000 --- a/client/js/common/client.js +++ /dev/null @@ -1,25 +0,0 @@ -// License: LGPL-3.0-or-later -// superapi wrapper with our api defaults - -var request = require('superagent') - -var wrapper = {} - -wrapper.post = function() { - return request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json') -} - -wrapper.put = function() { - return request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json') -} - -wrapper.del = function() { - return request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json') -} - -wrapper.get = function(path) { - return request.get.call(this, path).accept('json') -} - -module.exports = wrapper - diff --git a/client/js/common/colors.js b/client/js/common/colors.js deleted file mode 100644 index d646e6c1..00000000 --- a/client/js/common/colors.js +++ /dev/null @@ -1,59 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = { - // BLUES - '$dark-turquoise': "#306563" -, '$turquoise': "#a6d5d3" -, '$sea-foam': "#669092" -, '$light-sea-foam': "#5FA6AA" -, '$sky': "#97c2c4" -, '$faded-sky': "#B6D3D4" -, '$light-logo-blue': "#69B4CF" -, '$logo-blue': "#42B3DF" -, '$dark-logo-blue': "#479EBE" -, '$baby-blue': "#E2F8F8" -, '$blue-grey': "rgba(136, 148, 152, 1)" -, '$cloudy': "rgba(67, 164, 202, 0.3)" - - // GREENS -, '$light-grass': "#589E73" -, '$bluegrass': "#5FB88D" -, '$bluegrass--light': "lighten($bluegrass, 10)" -, '$grass': "#2d8f60" -, '$dark-grass': "#35865E" -, '$sage': "#E9F5E8" -, '$sage--dark': "#D5E4D4" -, '$thankYou-green': "#7EC981" -, '$mint': "#DEEFE7" - - // YELLOWS - ORANGES -, '$manila': "#FFFCE3" -, '$pollen': "#F0CD6C" -, '$light-pollen': "#FFE397" -, '$oj': "#FDC785" -, '$looseleaf': "lighten(#FFFEF6, 0.5)" - - // PINKS, REDS, PURPLES -, '$watermelon': "#EE8480" -, '$lavender': "#A57B9E" -, '$blush': "rgba(253, 168, 133, 0.1)" -, '$red': "#FF4F4F" - - // NEUTRALS -, '$charcoal': "#494949" -, '$charcoal--light': "lighten(#494949, 10)" -, '$grey': "rgb(128, 128, 128)" -, '$lightGrey': "lighten($grey, 20)" -, '$sepia': "rgba(65, 65, 65, 0.7)" -, '$shark': "rgb(162, 162, 162)" -, '$fog': "#fbfbfb" -, '$shade': "rgba(0,0,0,0.02)" - -, '$trans': "rgba(255,255,255,0)" - -, '$defaultShadow': "0 0 4px 1px rgba($grey, 0.5)" - - // SOCIAL -, '$facebook': "#236094" -, '$twitter': "#3199cb" -, '$google': "#dd4b39" -} diff --git a/client/js/common/confirmation.js b/client/js/common/confirmation.js deleted file mode 100644 index 73a218bd..00000000 --- a/client/js/common/confirmation.js +++ /dev/null @@ -1,46 +0,0 @@ -// License: LGPL-3.0-or-later -var confirmation = function(msg, success_cb) { - var $confirm_modal = $('#confirmation-modal') - var $msg = $confirm_modal.find('.msg') - if(msg && msg.length > 15) $msg.css('font-size', '16px') - var cb = { - confirmed: function() {}, - denied: function() {} - } - var $previousModal = $('.modal.inView') - $('.modal').removeClass('inView') - var $body = $('body') - $body.addClass('is-showingModal') - - function hide_confirmation_and_show_previous(){ - $('#confirmation-modal').removeClass('inView') - if ($previousModal.length){ - $previousModal.addClass('inView') - $body.addClass('is-showingModal') - } - else - $body.removeClass('is-showingModal') - } - - $confirm_modal.addClass('inView') - .off('click', '.yes') - .off('click', '.no') - - .on('click', '.yes', function(e) { - hide_confirmation_and_show_previous() - if(success_cb) { - success_cb() - } else { - cb.confirmed(e) - } - }) - .on('click', '.no', function(e) { - $('#confirmation-modal').removeClass('inView') - hide_confirmation_and_show_previous() - cb.denied(e) - }) - $msg.text(msg || 'Are you sure?') - return cb -} - -module.exports = confirmation diff --git a/client/js/common/credit-card-validator.js b/client/js/common/credit-card-validator.js deleted file mode 100644 index 832e6864..00000000 --- a/client/js/common/credit-card-validator.js +++ /dev/null @@ -1,25 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') - -// Reference: https://en.wikipedia.org/wiki/Luhn_algorithm - -module.exports = val => { - val = val.replace(/[-\s]/g, '') - return val.match(/^[0-9-\s]+$/) && luhnCheck(val) -} - -const luhnCheck = - R.compose( - R.equals(0) - , R.modulo(R.__, 10) - , R.sum - , R.map(n => n > 9 ? n - 9 : n) // Subtract 9 from those digits greater than 9 - , R.addIndex(R.map)((n, i) => i % 2 === 0 ? n : n * 2) // Double the value of every second digit - , R.map(ch => Number(ch)) - , R.reverse) - -/* -Luhn check in haskell: -luhn = (0 ==) . (`mod` 10) . sum . map (uncurry (+) . (`divMod` 10)) . - zipWith (*) (cycle [1,2]) . map digitToInt . reverse -*/ diff --git a/client/js/common/css-gradient.js b/client/js/common/css-gradient.js deleted file mode 100644 index 29ffcee8..00000000 --- a/client/js/common/css-gradient.js +++ /dev/null @@ -1,8 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = (dir, to, from) => -` background-image: -webkit-linear-gradient(${dir}, ${to}, ${from}); -background-image: -moz-linear-gradient(${dir}, ${to}, ${from}); -background-image: -ms-linear-gradient(${dir}, ${to}, ${from}); -background-image: linear-gradient(${dir}, ${to}, ${from}); -filter: progid:DXImageTransform.Microsoft.gradient(GradientType=1,startColorstr=${to}, endColorstr=${from});` - diff --git a/client/js/common/direct-to-s3-upload.es6 b/client/js/common/direct-to-s3-upload.es6 deleted file mode 100644 index 6e5adf54..00000000 --- a/client/js/common/direct-to-s3-upload.es6 +++ /dev/null @@ -1,41 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const R = require('ramda') - - -// local -const request = require('./super-agent-frp') -const postFormData = require('./post-form-data') - - -// Pass in a stream of Input Nodes with type file -// Make a post request to our server to start the import -// Will create a backgrounded job and email the user when -// completed -// Returns a stream of {uri: 'uri of uploaded file on s3', formData: 'original form data'} -const uploadFile = R.curry(input => { - // We need to get an AWS presigned post thing to so we can upload files - // Stream of pairs of [formObjData, presignedPostObj] - var withPresignedPost$ = flyd.map( - resp => [input, resp.body] - , request.post('/aws_presigned_posts').perform() - ) - - // Stream of upload responses from s3 - return flyd.flatMap( - pair => { - var [input, presignedPost] = pair - var url = `https://${presignedPost.s3_direct_url.host}` - var file = input.files[0] - var fileUrl = `${url}/tmp/${presignedPost.s3_uuid}/${file.name}` - var urlWithPort = `${url}:${presignedPost.s3_direct_url.port}` - var payload = R.merge(JSON.parse(presignedPost.s3_presigned_post), {file}) - - return flyd.map(resp => ({uri: fileUrl, file}), postFormData(url, payload)) - } - , withPresignedPost$) -}) - - -module.exports = uploadFile - diff --git a/client/js/common/dynamic_form.js b/client/js/common/dynamic_form.js deleted file mode 100644 index ffe66c8e..00000000 --- a/client/js/common/dynamic_form.js +++ /dev/null @@ -1,30 +0,0 @@ -// License: LGPL-3.0-or-later -var notification = require('./notification') - -$('form.dynamic').submit(function(e) { - var self = this - e.preventDefault() - var path = this.getAttribute('action') - var meth = this.getAttribute('method') - var form_data = new FormData(this) - $(this).find('button[type="submit"]').loading() - - $.ajax({ - type: meth, - url: path, - data: form_data, - dataType: 'json', - processData: false, - contentType: false - }) - .done(function(d) { - $('.modal').modal('hide') - notification(d.notification) - }) - .fail(function(d) { - $(self).find('.error').text(utils.print_error(d)) - }) - .complete(function() { - $(self).find('button[type="submit"]').disableLoading() - }) -}) diff --git a/client/js/common/editable.js b/client/js/common/editable.js deleted file mode 100644 index 2a0fb863..00000000 --- a/client/js/common/editable.js +++ /dev/null @@ -1,10 +0,0 @@ -// License: LGPL-3.0-or-later -// if you are instantiating more than one WYSIWYG on a page, -// be sure to give them id's to differentiate them -// to avoid unwanted display side effects - - -if (app.editor === 'froala') - module.exports = require('./editor/froala.es6') -else if (app.editor === 'quill') - module.exports = require('./editor/quill.es6') diff --git a/client/js/common/editor/froala.es6 b/client/js/common/editor/froala.es6 deleted file mode 100644 index 329a8b9d..00000000 --- a/client/js/common/editor/froala.es6 +++ /dev/null @@ -1,150 +0,0 @@ -// License: LGPL-3.0-or-later -var view = require("vvvview") -var savingIndicator = require('../../components/saving_indicator') -var savingState = {hide: true} -var renderSavingIndicator = view(savingIndicator, document.body, savingState) - -var donate_button_markup = "Donate" -else - donate_button_markup += ">Donate" - -var email_buttons = ["bold", "italic", "formatBlock", "align", "createLink", - "insertImage", "insertUnorderedList", "insertOrderedList", - "undo", "redo", "insert_donate_button", "insert_name", "html"] - -var froala = function($el, options) { - $el.editable({ - key: app.froala_key, - placeholder: options.placeholder || 'Edit text here', - buttons: options.email_buttons ? email_buttons : options.buttons ? options.buttons : ["bold", "italic", "formatBlock", "align", "createLink", "insertImage", "insertVideo", "insertUnorderedList", "insertOrderedList", "undo", "redo", "html"], - inlineMode: false, - beautifyCode: true, - plainPaste: true, - blockTags: {p: 'Normal', h5: "Heading", small: 'Caption'}, - allowedAttrs: ["accept","accept-charset","accesskey","action","align","alt","async","autocomplete","autofocus","autoplay","autosave","background","bgcolor","border","charset","cellpadding","cellspacing","checked","cite","class","color","cols","colspan","content","contenteditable","contextmenu","controls","coords","data","data-.*","datetime","default","defer","dir","dirname","disabled","download","draggable","dropzone","enctype","for","form","formaction","headers","height","hidden","high","href","hreflang","http-equiv","icon","id","ismap","itemprop","keytype","kind","label","lang","language","list","loop","low","max","maxlength","media","method","min","multiple", "muted", "name","novalidate","open","optimum","pattern","ping","placeholder","poster","preload","pubdate","radiogroup","readonly","rel","required","reversed","rows","rowspan","sandbox","scope","scoped","scrolling","seamless","selected","shape","size","sizes","span","src","srcdoc","srclang","srcset","start","step","summary","spellcheck","style","tabindex","target","title","type","translate","usemap","value","valign","width","wrap"], - imageUploadURL: '/image_attachments.json', - imageUploadParams: { - authenticity_token: $("meta[name='csrf-token']").attr('content') - }, - imageDeleteURL: '/image_attachments/remove.json', - imageErrorCallback: function (d) { - }, - afterRemoveImageCallback: function ($img) { - this.options.imageDeleteParams = {src: $img.attr('src')}; - this.deleteImage($img); - }, - customButtons: { - format_code: { - title: 'format code', - icon: { - type: 'font', - value: 'fa fa-bolt' - }, - callback: function () { - // used to show code snippets. - // takes selected text, including typed html tags - // and wraps each text line in a

- // and appends all of the
s into a
 tag
-                    // and then replaces that selected text with the
-                    // newly created 
 tag
-
-                    var lines_of_code = this.text().split("\n")
-                    var pre = document.createElement('pre')
-                    pre.className = 'codeText'
-
-                    // created 
s for each new line and appends them to
-                    lines_of_code.map(function (line) {
-                        var div = document.createElement('div')
-                        div.appendChild(document.createTextNode(line))
-                        pre.appendChild(div)
-                    })
-
-                    var selected_elements = this.getSelectionElements()
-                    var first_selected_element = selected_elements[0]
-                    var parent_node = document.getElementsByClassName('froala-element')[0]
-
-                    // inserts pre before selection
-                    parent_node.insertBefore(pre, first_selected_element)
-
-                    // inserts 
s before and after
-                    parent_node.insertBefore(document.createElement('br'), pre)
-                    parent_node.insertBefore(document.createElement('br'), pre.nextSibling)
-
-                    // deletes selection
-                    selected_elements.map(function (el) {
-                        parent_node.removeChild(el)
-                    })
-
-                    this.saveUndoStep()
-                }
-            },
-            insert_donate_button: {
-                title: 'Donate Button',
-                icon: {
-                    type: 'font',
-                    value: 'fa fa-heart'
-                },
-                callback: function () {
-                    this.insertHTML(donate_button_markup)
-                    this.saveUndoStep()
-                },
-                refresh: function () {
-                }
-            },
-            insert_name: {
-                title: 'Insert recipient name',
-                icon: {
-                    type: 'txt',
-                    value: 'Name'
-                },
-                callback: function () {
-                    this.insertHTML("{{NAME}}")
-                    this.saveUndoStep()
-                },
-                refresh: function () {
-                }
-            },
-        },
-      videoAllowedAttrs: ["src","width","height","frameborder","allowfullscreen","webkitallowfullscreen","mozallowfullscreen","href","target","id","controls","value","name", "autoplay", "loop", "muted"]
-    })
-
-    $('.froala-popup').parents('.froala-editor').css('z-index', 99999)
-
-    if (!options.noUpdateOnChange) {
-        $el.on('editable.contentChanged', function (e, editor) {
-            utils.delay(100, function () {
-                var key = $el.data('key')
-                var data = {}
-                var path = $el.data('path')
-                data[key] = $el.find('.froala-element').html()
-                renderSavingIndicator({hide: false, text: 'Saving...'})
-                $.ajax({type: 'put', url: path, data: data})
-                    .done(function () {
-                        renderSavingIndicator({text: 'Saved'})
-                        window.setTimeout(function () {
-                            renderSavingIndicator({hide: true})
-                        }, 500)
-                    })
-            })
-        })
-    }
-
-    if (options.sticky) {
-        window.onload = function () {
-            var makeEditorStick = require('../scroll_toggle_class')
-            var id = $el.attr('id') ? '#' + $el.attr('id') : false
-            var parent = id ? id : '.froala-box'
-            var child = id ? id + ' .froala-editor' : '.froala-editor'
-            makeEditorStick(child, 'is-stuck', parent)
-            $(child).css('width', $(parent).width())
-            $(window).resize(function () {
-                $(child).css('width', $(parent).width())
-            })
-        }
-    }
-}
-
-module.exports = froala;
\ No newline at end of file
diff --git a/client/js/common/editor/quill.es6 b/client/js/common/editor/quill.es6
deleted file mode 100644
index 9c2a6517..00000000
--- a/client/js/common/editor/quill.es6
+++ /dev/null
@@ -1,45 +0,0 @@
-// License: LGPL-3.0-or-later
-var view = require("vvvview")
-var savingIndicator = require('../../components/saving_indicator')
-var savingState = {hide: true}
-var renderSavingIndicator = view(savingIndicator, document.body, savingState)
-
-
-const Quill = require('quill')
-
-function initializeQuill($el, options)
-{
-    var editor = new Quill($el, {
-        theme: 'bubble',
-        placeholder: options.placeholder
-    });
-
-    if (!options.noUpdateOnChange) {
-        editor.on('text-change', function () {
-            utils.delay(100, function () {
-                var key = $el.getAttribute('data-key')
-                var data = {}
-                var path = $el.getAttribute('data-path')
-                data[key] = editor.root.innerHTML
-                renderSavingIndicator({hide: false, text: 'Saving...'})
-                $.ajax({type: 'put', url: path, data: data})
-                    .done(function () {
-                        renderSavingIndicator({text: 'Saved'})
-                        window.setTimeout(function () {
-                            renderSavingIndicator({hide: true})
-                        }, 500)
-                    })
-            })
-        })
-    }
-}
-
-
-var quill = function($el, options) {
-    for (var i =0; i < $el.length; i++)
-    {
-        initializeQuill($el[i], options)
-    }
-}
-
-module.exports = quill
\ No newline at end of file
diff --git a/client/js/common/el_swapo.js b/client/js/common/el_swapo.js
deleted file mode 100644
index 9047af10..00000000
--- a/client/js/common/el_swapo.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// License: LGPL-3.0-or-later
-var el_swapo = {}
-
-$('*[swap-in]').each(function(i) {
-	var self = this
-
-	$(this).on('click', function(e) {
-		var swap_class = self.getAttribute('swap-class')
-		var new_class = self.getAttribute('swap-in')
-		swap(swap_class, new_class)
-	})
-})
-
-function swap(swap_class, new_class) {
-	$('*[swap-class="' + swap_class + '"]').removeClass('active')
-	$('*[swap-in="' + new_class + '"]').addClass('active')
-	$('.' + swap_class).hide()
-	$('.' + new_class).fadeIn()
-	utils.change_url_param('p', new_class)
-	utils.change_url_param('s', swap_class)
-}
-
-var current_page = utils.get_param('p')
-var current_swap = utils.get_param('s')
-if(current_page && current_swap) {
-	swap(current_swap, current_page)
-  setTimeout(() => document.querySelector(`[swap-in='${current_page}']`).click(), 400)
-}
-
-module.exports = el_swapo
diff --git a/client/js/common/event.js b/client/js/common/event.js
deleted file mode 100644
index 934f1c87..00000000
--- a/client/js/common/event.js
+++ /dev/null
@@ -1,14 +0,0 @@
-// License: LGPL-3.0-or-later
-var actions = [ 'change', 'click', 'dblclick', 'mousedown', 'mouseup', 'mouseenter', 'mouseleave', 'scroll', 'blur', 'focus', 'input', 'submit', 'keydown', 'keypress', 'keyup' ]
-
-function event(id, fn) {
-	// Find all classes ending in the event id
-	actions.forEach(function(action) {
-		$('*[on-' + action + '="' + id + '"]').each(function() {
-			if(this.getAttribute('on-' + action).indexOf(id) !== -1)
-				$(this).on(action, fn)
-		})
-	})
-}
-
-module.exports = event
diff --git a/client/js/common/ff-form-validation/index.es6 b/client/js/common/ff-form-validation/index.es6
deleted file mode 100644
index bbd6ea85..00000000
--- a/client/js/common/ff-form-validation/index.es6
+++ /dev/null
@@ -1,180 +0,0 @@
-// License: LGPL-3.0-or-later
-const h = require('snabbdom/h')
-const R = require('ramda')
-const flyd = require('flyd')
-const serializeForm = require('form-serialize')
-flyd.filter = require('flyd/module/filter')
-flyd.mergeAll = require('flyd/module/mergeall')
-flyd.keepWhen = require('flyd/module/keepwhen')
-flyd.sampleOn = require('flyd/module/sampleon')
-const readableProp = require('./lib/readable-prop.es6')
-const emailRegex = require('./lib/email-regex.es6')
-const currencyRegex = require('./lib/currency-regex.es6')
-
-// constraints: a hash of key/vals where each key is the name of an input
-// and each value is an object of validator names and arguments
-//
-// validators: a hash of validation names mapped to boolean functions
-//
-// messages: a hash of validator names and field names mapped to error messages 
-//
-// messages match on the most specific thing in the messages hash
-// - first checks if there is an exact match on field name
-// - then checks for match on validator name
-//
-// Given a constraint like:
-// {name: {required: true}}
-//
-// All of the following will set an error for above, starting with most specific first:
-// {name: {required: 'Please enter a valid name'}
-// {name: 'Please enter your name'}
-// {required: 'This is required'}
-
-
-function init(state) {
-  state = R.merge({
-    validators: R.merge(defaultValidators, state.validators || {})
-  , messages: R.merge(defaultMessages, state.messages || {})
-  , focus$:  flyd.stream()
-  , change$: flyd.stream()
-  , submit$: flyd.stream()
-  }, state || {})
-  const valField = validateField(state)
-  const valForm  = validateForm(state)
-
-  const fieldErr$ = flyd.map(valField, state.change$)
-  const formErr$ = flyd.map(valForm, state.submit$)
-  const clearErr$ = flyd.map(ev => [ev.target.name, null], state.focus$)
-  const allErrs$ = flyd.mergeAll([fieldErr$, formErr$, clearErr$])
-
-  // Stream of all errors combined into one object
-  state.errors$ = flyd.scan(R.assoc, {}, allErrs$)
-
-  // Stream of field names and new values and whole form data
-  state.nameVal$ = flyd.map(node => [node.name, node.value], state.change$)
-  state.data$ = flyd.scan(R.assoc, {}, state.nameVal$)
-
-  // Streams of errors and data on form submit
-  const errorsOnSubmit$ = flyd.sampleOn(state.submit$, state.errors$)
-  state.validSubmit$ = flyd.filter(R.compose(R.none, R.values), errorsOnSubmit$)
-  state.validData$ = flyd.keepWhen(state.validSubmit$, state.data$)
-  
-  return state
-}
-
-
-// Pass in an array of validation functions and the event object
-// Will return a pair of [name, errorMsg] (errorMsg will be null if no errors present)
-const validateField = R.curry((state, node) => {
-  const value = node.value
-  const name = node.name
-  if(!state.constraints[name]) return [name, null] // no validators for this field present
-
-  // Find the first constraint that fails its validator 
-  for(var valName in state.constraints[name]) {
-    const arg = state.constraints[name][valName]
-    if(!state.validators[valName]) {
-      console.warn("Form validation constraint does not exist:", valName)
-    } else if(!validators[valName](value, arg)) {
-      const msg = getErr(messages, name, valName, arg)
-      return [name, String(msg)]
-    }
-  }
-  return [name, null] // no error found
-})
-
-
-// Given the messages object, the validator argument, the field name, and the validator name
-// Retrieve and apply the error message function
-const getErr = (messages, name, valName, arg) => {
-  const err = messages[name] 
-    ? messages[name][valName] || messages[name]
-    : messages[valName]
-  if(typeof err === 'function') return err(arg)
-  else return err
-}
-
-
-// Retrieve errors for the entire set of form data, used on form submit events,
-// using the form data saved into the state
-const validateForm = R.curry((state, node) => {
-  const formData = serializeForm(node, {hash: true})
-  for(var fieldName in constraints) { // not using higher order .map or reduce so we can break and return early
-    for(var valName in constraints[fieldName]) {
-      const arg = constraints[fieldName][valName]
-      if(!validators[valName]) {
-        console.warn("Form validation constraint does not exist:", valName)
-      } else if(!validators[valName](value, arg)) {
-        const msg = getErr(messages, name, valName, arg)
-        return [name, String(msg)]
-      }
-    }
-  }
-}
-
-
-// -- Views
-
-const validatedForm = R.curry((state, elm) => {
-  elm.data = R.merge(elm.data, {
-    on: {submit: ev => {ev.preventDefault(); state.submit$(ev.currentTarget)}}
-  })
-  return elm
-})
-
-
-// A single form field
-// Data takes normal snabbdom data for the input/select/textarea (eg props, style, on)
-const validatedField = R.curry((state, elm) => {
-  if(!elm.data.props || !elm.data.props.name) throw new Error(`You need to provide a field name for validation (using the 'props.name' property)`)
-  var err = state.errors$()[elm.data.props.name]
-  var invalid = err && err.length
-
-  elm.data = R.merge(elm.data, {
-    on: {
-      focus: state.focus$
-    , change: ev => state.change$([ev.currentTarget, state])
-    }
-  , class: { invalid }
-  })
-
-  return h('div', {
-    props: {className: 'ff-field' + (invalid ? ' ff-field--invalid' : ' ff-field--valid')}
-  }, [
-    invalid ? h('p.ff-field-errorMessage', err) : ''
-  , elm
-  ])
-})
-
-var defaultMessages = {
-  email: 'Please enter a valid email address'
-, required: 'This field is required'
-, currency: 'Please enter valid currency'
-, format: "This doesn't look like the right format"
-, isNumber: 'This should be a number'
-, max: n => `This should be less than ${n}`
-, min: n => `This should be at least ${n}`
-, equalTo: n => `This should be equal to ${n}`
-, maxLength: n => `This should be no longer than ${n}`
-, minLength: n => `This should be longer than ${n}`
-, lengthEquals: n => `This should have a length of ${n}`
-, includedIn: arr => `This should be one of: ${arr.join(', ')}`
-}
-
-var defaultValidators = {
-  email: val => val.match(emailRegex)
-, present: val => Boolean(val)
-, currency: val => String(val).match(currencyRegex)
-, format: (val, arg) => String(val).match(arg)
-, isNumber: val => !isNaN(val)
-, max: (val, n) => val <= n
-, min: (val, n) => val >= n
-, equalTo:  (val, n) => n === val
-, maxLength: (val, n) => val.length <= n
-, minLength: (val, n) => val.length >= n
-, lengthEquals: (val, n) => val.length === n
-, includedIn: (val, arr) => arr.indexOf(val) !== -1
-}
-
-module.exports = {init, validatedField, validatedForm}
-
diff --git a/client/js/common/ff-form-validation/lib/currency-regex.es6 b/client/js/common/ff-form-validation/lib/currency-regex.es6
deleted file mode 100644
index 378586ca..00000000
--- a/client/js/common/ff-form-validation/lib/currency-regex.es6
+++ /dev/null
@@ -1,3 +0,0 @@
-// License: LGPL-3.0-or-later
-module.exports = /^(?!0\.00)\d{1,3}(,\d{3})*(\.\d\d)?$/
-
diff --git a/client/js/common/ff-form-validation/lib/email-regex.es6 b/client/js/common/ff-form-validation/lib/email-regex.es6
deleted file mode 100644
index 624a9afa..00000000
--- a/client/js/common/ff-form-validation/lib/email-regex.es6
+++ /dev/null
@@ -1,4 +0,0 @@
-// License: LGPL-3.0-or-later
-// http://stackoverflow.com/questions/46155/validate-email-address-in-javascript
-module.exports = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
- 
diff --git a/client/js/common/ff-form-validation/lib/readable-prop.es6 b/client/js/common/ff-form-validation/lib/readable-prop.es6
deleted file mode 100644
index 2f1985ff..00000000
--- a/client/js/common/ff-form-validation/lib/readable-prop.es6
+++ /dev/null
@@ -1,12 +0,0 @@
-// License: LGPL-3.0-or-later
-// "email_address" => "email address"
-// "emailAddress" => "email address"
-
-module.exports = str =>
-  str
-  .replace('_', ' ')
-  .replace(/([a-z])([A-Z])/g, '$1 $2')
-  .replace(/\b([A-Z]+)([A-Z])([a-z])/, '$1 $2$3')
-  .toLowerCase()
-
-
diff --git a/client/js/common/file-input-stream.js b/client/js/common/file-input-stream.js
deleted file mode 100644
index 4f506b04..00000000
--- a/client/js/common/file-input-stream.js
+++ /dev/null
@@ -1,17 +0,0 @@
-// License: LGPL-3.0-or-later
-const flyd = require('flyd')
-const R = require('ramda')
-
-// Given an input element, return a stream of the input file data as text
-
-module.exports = R.curry(node => {
-  var $stream = flyd.stream()
-  var file = node.files[0]
-  var reader = new FileReader()
-  if(file instanceof Blob) {
-    reader.readAsText(file)
-    reader.onload = e => $stream(reader.result)
-  }
-  return $stream
-})
-
diff --git a/client/js/common/form-to-object.js b/client/js/common/form-to-object.js
deleted file mode 100644
index 8321ff9f..00000000
--- a/client/js/common/form-to-object.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// License: LGPL-3.0-or-later
-// Convert a form to an object literal
-module.exports = function(form) {
-	if(form === undefined) throw new Error("form is undefined")
-	var result = {}
-	var fields = toArr(form.querySelectorAll('input, textarea, select'))
-  .filter(function(n) { return n.hasAttribute('name') })
-  .map(function(n) {
-    var name = n.getAttribute('name')
-    var keys = n.getAttribute('name').split('.')
-    if(n.value && n.value.toString().length) { // won't set empty strings for empty vals
-      if(n.getAttribute('type') === 'checkbox') {
-        deepSet(keys, n.checked, result)
-      } else if(n.getAttribute('type') === 'radio') {
-        if(n.checked) deepSet(keys, n.value, result)
-      } else {
-        deepSet(keys, n.value, result)
-      }
-    }
-  })
-
-	return result
-}
-
-function toArr(x) { return Array.prototype.slice.call(x) }
-
-// Given an array of nested keys, a value, and a target object:
-// Set the value into the object at the last nested key
-function deepSet(keys, val, obj, options) {
-	var exceptLast = keys.slice(0, keys.length-1)
-	var last = keys[keys.length-1]
-	var nested = exceptLast.reduce(function(nestedObj, key) {
-		if(nestedObj[key] === undefined) {
-			nestedObj[key] = {}
-			return nestedObj[key]
-		} else {
-			return nestedObj[key]
-		}
-	}, obj)
-	// if(nested[last] === undefined) nested[last] = {}
-	nested[last] = val
-	return obj
-}
-
diff --git a/client/js/common/form.js b/client/js/common/form.js
deleted file mode 100644
index ace9efcc..00000000
--- a/client/js/common/form.js
+++ /dev/null
@@ -1,20 +0,0 @@
-// License: LGPL-3.0-or-later
-var form = module.exports = {
-	loading: loading,
-	showErr: showErr,
-	clear: clear
-}
-
-function loading(formEl) {
-	$(formEl).find('button[type="submit"]').loading()
-}
-
-function showErr(msg, el) {
-	$(el).find('.status').addClass('error').text(msg)
-	$(el).find('button[type="submit"]').disableLoading()
-}
-
-function clear(el) {
-	$(el).find('.status').removeClass('error').text('')
-	$(el).find('button[type="submit"]').disableLoading()
-}
diff --git a/client/js/common/format.js b/client/js/common/format.js
deleted file mode 100644
index dc4fe828..00000000
--- a/client/js/common/format.js
+++ /dev/null
@@ -1,126 +0,0 @@
-// License: LGPL-3.0-or-later
-var moment = require('moment')
-var format = {}
-
-module.exports = format
-
-// Convert a snake-case phrase (eg. 'requested_by_customer') to a readable phrase (eg. 'Requested by customer')
-format.snake_to_words = function(snake, options) {
-	if(!snake) return snake
-	return snake.replace(/_/g, ' ').replace(/^./, function(m) {return m.toUpperCase()})
-}
-
-format.camelToWords = function(str, os) {
-  if(!str) return str
-  return str.replace(/([A-Z])/g, " $1")
-}
-
-format.dollarsToCents = function(dollars) {
-	dollars = dollars.toString().replace(/[$,]/g, '')
-    if(!isNaN(dollars) && dollars.match(/^-?\d+\.\d$/)) {
-        // could we use toFixed instead? Probably but this is straightforward.
-        dollars = dollars + "0"
-    }
-	if(isNaN(dollars) || !dollars.match(/^-?\d+(\.\d\d)?$/)) throw "Invalid dollar amount: " + dollars
-  return Math.round(Number(dollars) * 100)
-}
-
-format.centsToDollars = function(cents, options={}) {
-	if(cents === undefined) return '0'
-	return format.numberWithCommas((Number(cents) / 100.0).toFixed(options.noCents ? 0 : 2).toString()).replace(/\.00$/,'')
-}
-
-format.weeklyToMonthly = function(amount) {
-  if (amount === undefined) return 0;
-  return Math.round(4.3 * amount);
-}
-
-
-
-format.numberWithCommas = function(n) {
-  return String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ",")
-}
-
-format.percent = function(x, y) {
-  if(!x || !y) return 0
-  return Math.round(y / x * 100)
-}
-
-format.pluralize = function(quantity, plural_word) {
-	if(quantity === undefined || quantity === null) return '0 '+plural_word
-	var str = String(quantity) + ' '
-	if(quantity !== 1) return str+plural_word
-	else return str + appl.to_singular(plural_word)
-}
-
-format.capitalize = function (string) {
-  return string.split(' ')
-    .map(function(s) { return s.charAt(0).toUpperCase() + s.slice(1) })
-    .join(' ')
-}
-
-format.toSentence = function(arr) {
-	if(arr.length < 2) return arr
-	if(arr.length === 2) return arr[0] + ' and ' + arr[1]
-	var last = arr.length - 1
-	return arr.slice(0, last).join(', ') + ', and ' + arr[last]
-}
-
-format.zeroPad = function(num, size) {
-	var str = num + ""
-	while(str.length < size) str = "0" + str
-	return str
-}
-
-format.sanitizeHtml = function(html) {
-  if(!html) return
-  var tagBody = '(?:[^"\'>]|"[^"]*"|\'[^\']*\')*'
-  var tagOrComment = new RegExp(
-    '<(?:'
-      // Comment body.
-      + '!--(?:(?:-*[^->])*--+|-?)'
-      // Special "raw text" elements whose content should be elided.
-      + '|script\\b' + tagBody + '>[\\s\\S]*?[\\s\\S]*?',
-    'gi')
-  return html.replace(tagOrComment, '').replace(/ {
-  const url = '/'
-  const response$ = request({method: 'GET', url, path, query}).load
-  const valid$ = flyd.filter(x => x.status === 200, response$)
-  return flyd.map(x => x.body, valid$)
-}
-
diff --git a/client/js/common/image_uploader.js b/client/js/common/image_uploader.js
deleted file mode 100644
index 7bec14a4..00000000
--- a/client/js/common/image_uploader.js
+++ /dev/null
@@ -1,34 +0,0 @@
-// License: LGPL-3.0-or-later
-$('.image-upload input').change(function(e) {
-	var self = this
-	appl.def('image_upload.is_selecting', true)
-	if(this.files && this.files[0]) {
-		var reader = new FileReader()
-		reader.onload = function(e) {
-			if(e.valueOf().loaded >= 3000000) {
-				appl.def('image_upload.error', 'Please select a file smaller than 3mb')
-			} else {
-				appl.def('image_upload.error', '')
-			}
-			$(self).parent().css('background-image', "url('" + e.target.result + "')")
-			$(self).parent().addClass('live-preview')
-		}
-		reader.readAsDataURL(this.files[0])
-	}
-})
-
-appl.def('remove_image', function(url, resource, notification, payload) {
-	var data = {}
-	data[resource] = payload
-	appl.notify(notification)
-	appl.def('loading', true)
-	$.ajax({
-		type: 'put',
-		url: url,
-		data: data,
-	})
-		.done(function() {
-			appl.reload()
-		})
-		.fail(function(e) { })
-})
diff --git a/client/js/common/jquery_additions.js b/client/js/common/jquery_additions.js
deleted file mode 100644
index 640f137a..00000000
--- a/client/js/common/jquery_additions.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// License: LGPL-3.0-or-later
-$.fn.serializeObject = function() {
-	return this.serializeArray().reduce(function(obj, field) {
-		if(field.value)
-			var val = field.value
-		else if(field.files && field.files[0])
-			var val = field.files[0]
-		obj[field.name] = val
-		return obj 
-	}, {})
-}
-
-// Make a button enter the ajax loading state, where it's disabled and has a little spinner.
-$.fn.loading = function(message) {
-	this.each(function() {
-		var msg = message || this.getAttribute('data-loading')
-		this.setAttribute('data-text', this.innerHTML)
-		this.innerHTML = " " + msg
-		this.setAttribute('disabled', 'disabled')
-	})
-	return this
-}
-
-$.fn.disableLoading = function() {
-	this.each(function() {
-		if(!this.hasAttribute('disabled')) return
-		var old_text = this.getAttribute('data-text')
-		this.innerHTML = old_text
-		this.removeAttribute('disabled')
-	})
-	return this
-}
diff --git a/client/js/common/notification.js b/client/js/common/notification.js
deleted file mode 100644
index 4ca5c0b5..00000000
--- a/client/js/common/notification.js
+++ /dev/null
@@ -1,13 +0,0 @@
-// License: LGPL-3.0-or-later
-var notification = function(msg, err) {
-	var el = document.getElementById('js-notification')
-  if(err) {el.className = 'show error'} 
-  else {el.className = 'show'}
-  el.innerText = msg
-	window.setTimeout(function() {
-		el.className = ''
-    el.innerText = ''
-	}, 7000)
-}
-module.exports = notification
-
diff --git a/client/js/common/on-change-sanitize-slug.js b/client/js/common/on-change-sanitize-slug.js
deleted file mode 100644
index 126dde2a..00000000
--- a/client/js/common/on-change-sanitize-slug.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: LGPL-3.0-or-later
-const R = require('ramda')
-const sanitize = require('./sanitize-slug')
-
-// Just a hacky way to automatically sanitize slug inputs when they are changed
-
-var inputs = document.querySelectorAll('.js-sanitizeSlug')
-
-R.map(
-  inp => inp.addEventListener('change', ev => ev.currentTarget.value = sanitize(ev.currentTarget.value || ev.currentTarget.getAttribute('data-slug-default')))
-, inputs )
diff --git a/client/js/common/on-ios11.js b/client/js/common/on-ios11.js
deleted file mode 100644
index 99728c18..00000000
--- a/client/js/common/on-ios11.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: LGPL-3.0-or-later
-function calculateIOS()
-{
-    var userAgent = window.navigator.userAgent;
-    var has11 = userAgent.search("OS 11_\\d") > 0
-    var hasMacOS = userAgent.search(" like Mac OS X") > 0
-
-    return has11 && hasMacOS;
-}
-
-module.exports = calculateIOS
\ No newline at end of file
diff --git a/client/js/common/onboard.js b/client/js/common/onboard.js
deleted file mode 100644
index bdcfbbc9..00000000
--- a/client/js/common/onboard.js
+++ /dev/null
@@ -1,280 +0,0 @@
-// License: LGPL-3.0-or-later
-const flyd = require('flimflam/flyd')
-const h = require('flimflam/h')
-const R = require('ramda')
-const modal = require('flimflam/ui/modal')
-const render = require('flimflam/render')
-const wizard = require('flimflam/ui/wizard')
-const validatedForm = require('flimflam/ui/validated-form')
-const request = require('./request')
-const notification = require('flimflam/ui/notification')
-const fieldWithError = require('../components/field-with-error')
-
-const init = () => {
-  const orgForm = validatedForm.init({constraints: constraints.org})
-  const contactForm = validatedForm.init({constraints: constraints.contact})
-  const infoForm = validatedForm.init({constraints: constraints.info})
-  const currentStep$ = flyd.mergeAll([
-    flyd.stream(0)
-  , flyd.map(R.always(1), orgForm.validSubmit$)
-  , flyd.map(R.always(2), infoForm.validSubmit$)
-  ])
-  const wiz = wizard.init({currentStep$})
-  const openModal$ = flyd.stream()
-  document.querySelectorAll('[data-ff-open-onboard]')
-    .forEach(x => {x.addEventListener('click', openModal$)})
-
-  //flyd.map(trackGA, openModal$)
-
-  const response$ = flyd.flatMap(postData(orgForm, infoForm, contactForm), contactForm.validData$)
-  const respOk$ = flyd.filter(resp => resp.status === 200, response$)
-  const respErr$ = flyd.filter(resp => resp.status !== 200, response$)
-  const loading$ = flyd.mergeAll([
-    flyd.map(R.always(true), contactForm.validSubmit$)
-  , flyd.map(R.always(false), response$)
-  ])
-
-  const message$ = flyd.mergeAll([
-    flyd.map(R.always("Saving your data..."), contactForm.validSubmit$)
-  , flyd.map(R.always("Thank you! Now redirecting..."), respOk$)
-  , flyd.map(resp => `There was an error: ${resp.body.error}`, respErr$)
-  ])
-
-  const notif = notification.init({message$, hideDelay: 20000})
-  flyd.map(resp => {setTourCookies(resp.body.nonprofit); window.location = '/'}, respOk$)
-
-
-  return {
-    openModal$
-  , currentStep$
-  , wiz
-  , orgForm
-  , contactForm
-  , infoForm
-  , loading$
-  , notif
-  }
-}
-
-// const trackGA = () => {
-//   if(!ga) return
-//   ga('send', {
-//     hitType: 'event',
-//     eventCategory: 'ClickSignUp',
-//     eventAction: 'click',
-//     eventLabel: location.pathname
-//   })
-// }
-
-
-const setTourCookies = nonprofit => {
-  document.cookie = `tour_dashboard=${nonprofit.id};path=/`
-  document.cookie = `tour_campaign=${nonprofit.id};path=/`
-  document.cookie = `tour_event=${nonprofit.id};path=/`
-  document.cookie = `tour_profile=${nonprofit.id};path=/`
-  document.cookie = `tour_transactions=${nonprofit.id};path=/`
-  document.cookie = `tour_supporters=${nonprofit.id};path=/`
-  document.cookie = `tour_subscribers=${nonprofit.id};path=/`
-}
-
-const postData = (orgForm, infoForm) => contactFormData => {
-  const send = {
-    nonprofit: orgForm.validData$()
-  , extraInfo: infoForm.validData$()
-  , user: contactFormData
-  }
-  return request({
-    method: 'post'
-  , path: '/nonprofits/onboard'
-  , send
-  }).load
-}
-
-const constraints = {
-  org: {
-    name: {required: true}
-  , city: {required: true}
-  , state_code: {required: true}
-  , zip_code: {required: true}
-  }
-, contact: {
-    email: {required: true, email: true}
-  , name: {required: true}
-  , phone: {required: true}
-  , password: {required: true, minLength: 7}
-  , password_confirmation: {required: true, matchesField: 'password'}
-  }
-, info: {}
-}
-
-const view = state => {
-  return h('div', [
-    modal({
-      show$: state.openModal$
-    , body: onboardWizard(state)
-    , title: 'Get started'
-    })
-  , notification.view(state.notif)
-  ])
-}
-
-const onboardWizard = state => {
-  const labels = [ 'Org', 'Info', 'Contact' ]
-  const steps = [ orgForm(state) , infoForm(state), contactForm(state) ]
-  return h('div', [ 
-    wizard.labels(state.wiz, labels)
-  , wizard.content(state.wiz, steps)
-  ])
-}
-
-const pricingDetails = h('div.u-marginTop--15.u-padding--10.u-background--fog', [
-  h('p', [
-    "CommitChange uses " 
-  , h('a.strong', {props: {href: 'https://www.stripe.com/', target :'_blank'}}, 'Stripe')
-  , ' to process transactions. Stripe takes a '
-  , h('strong', `${ENV.feeRate}% + ${ENV.perTransaction}¢`) 
-  , ' processing fee on every transaction.'])
-, h('p', [
-    'In order to support operations, feature development, and community building, '
-  , 'CommitChange takes an additional fee of ' 
-  , h('strong', `${ENV.platformFeeRate}%.`) 
-  ])
-, h('p.u-marginBottom--0', [
-  "Our fee scales down as your transaction volume scales up. "
-, h('a.strong', {props: {href: 'mailto:support@commitchange.com'}}, 'Contact us')
-, " to chat about volume discounts."
-  ])
-])
-
-const orgForm = state => {
-  const form = validatedForm.form(state.orgForm)
-  const field = fieldWithError(state.orgForm)
-  return h('div', [
-    form(h('form', [
-     h('fieldset', [
-        h('label', 'Organization Name')
-      , field(h('input', {props: {type: 'text', name: 'name', placeholder: ''}}))
-      ])
-    , h('fieldset', [
-        h('label', 'Website URL')
-      , field(h('input', {props: {type: 'text', name: 'website', placeholder: 'https://your-website.org'}}))
-      ])
-    , h('div.clearfix', [
-        h('fieldset.col-left-6.u-paddingRight--10', [
-          h('label', 'Org Email (public)')
-        , field(h('input', {props: {type: 'email', name: 'email', placeholder: 'example@name.org'}}))
-        ])
-      , h('fieldset.col-left-6', [
-          h('label', 'Org Phone (public)')
-        , field(h('input', {props: {type: 'text', name: 'phone', placeholder: '(XXX) XXX-XXXX'}}))
-        ])
-      ])
-    , h('div.clearfix', [
-        h('fieldset.col-left-6.u-paddingRight--10', [
-          h('label', 'City')
-        , field(h('input', {props: {type: 'text', name: 'city', placeholder: ''}}))
-        ])
-      , h('fieldset.col-left-3.u-paddingRight--10', [
-          h('label', 'State')
-        , field(h('input', {props: {type: 'text', name: 'state_code', placeholder: 'NY'}}))
-        ])
-      , h('fieldset.col-left-3', [
-          h('label', 'Zip Code')
-        , field(h('input', {props: {type: 'text', name: 'zip_code', placeholder: ''}}))
-        ])
-      ])
-    , h('div', [
-        h('button.button', 'Next')
-      ])
-    ]))
-  ])
-}
-
-const infoForm = state => {
-  const form = validatedForm.form(state.infoForm)
-  const field = fieldWithError(state.infoForm)
-
-  return h('div', [
-    form(h('form', [
-      h('div.u-marginBottom--20', [
-        h('fieldset', [
-          h('label', {props: {htmlFor: 'registered-npo-checkbox'}}, 'What kind of entity are you fundraising for?')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'radio', name: 'entity_type', value: 'nonprofit', id: 'onboard-entity-nonprofit'}})
-        , h('label', {props: {htmlFor: 'onboard-entity-nonprofit'}}, 'A registered nonprofit')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'radio', name: 'entity_type', value: 'forprofit', id: 'onboard-entity-forprofit'}})
-        , h('label', {props: {htmlFor: 'onboard-entity-forprofit'}}, 'A for-profit company')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'radio', name: 'entity_type', value: 'unregistered', id: 'onboard-entity-unregistered'}})
-        , h('label', {props: {htmlFor: 'onboard-entity-unregistered'}}, 'An unregistered project, group, club, or other cause')
-        ])
-      ])
-    , h('div.u-marginBottom--20', [
-        h('fieldset', [
-          h('label', 'How do you want to use CommitChange?')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'checkbox', name: 'use_donations', id: 'onboard-use-donations'}})
-        , h('label', {props: {htmlFor: 'onboard-use-donations'}}, 'Donation processing')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'checkbox', name: 'use_crm', id: 'onboard-use-crm'}})
-        , h('label', {props: {htmlFor: 'onboard-use-crm'}}, 'Supporter relationship management')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'checkbox', name: 'use_campaigns', id: 'onboard-use-campaigns'}})
-        , h('label', {props: {htmlFor: 'onboard-use-campaigns'}}, 'Campaign fundriasing')
-        ])
-      , h('fieldset', [
-          h('input', {props: {type: 'checkbox', name: 'use_events', id: 'onboard-use-events'}})
-        , h('label', {props: {htmlFor: 'onboard-use-events'}}, 'Event pages and ticketing')
-        ])
-      ])
-    , h('fieldset', [
-        h('label', 'How did you hear about CommitChange?')
-      , field(h('input', {props: {type: 'text', name: 'how_they_heard', placeholder: 'Google, radio, referral, etc'}}))
-      ])
-    , h('button.button', 'Next')
-    ]))
-  ])
-}
-
-const contactForm = state => {
-  const form = validatedForm.form(state.contactForm)
-  const field = fieldWithError(state.contactForm)
-  return h('div', [
-    form(h('form', [
-      h('div.clearfix', [
-        h('fieldset.col-left-6.u-paddingRight--10', [
-          h('label', 'Your Name')
-        , field(h('input', {props: {type: 'text', name: 'name', placeholder: 'Full Name'}}))
-        ])
-      , h('fieldset.col-left-6', [
-          h('label', 'Your Email (used for login)')
-        , field(h('input', {props: {type: 'email', name: 'email', placeholder: 'youremail@example.com'}}))
-        ])
-      ])
-    , h('fieldset', [
-        h('label', 'New Password')
-      , field(h('input', {props: {type: 'password', name: 'password', placeholder: ''}}))
-      ])
-    , h('fieldset', [
-        h('label', 'Retype Password')
-      , field(h('input', {props: {type: 'password', name: 'password_confirmation', placeholder: ''}}))
-      ])
-    , h('fieldset', [
-        h('label', ['Your Phone', h('small', ' (for account recovery)')])
-      , field(h('input', {props: {type: 'text', name: 'phone', placeholder: '(XXX) XXX-XXXX'}}))
-      ])
-    , h('button.button', {props: {disabled: state.loading$()}}, 'Save & Finish')
-    ]))
-  ])
-}
-
-const container = document.querySelector("#ff-render-onboard")
-render(view, init(), container)
-
diff --git a/client/js/common/panels_layout.js b/client/js/common/panels_layout.js
deleted file mode 100644
index 6da6a7e5..00000000
--- a/client/js/common/panels_layout.js
+++ /dev/null
@@ -1,71 +0,0 @@
-// License: LGPL-3.0-or-later
-var $panelsLayout = $('.panelsLayout'),
-	$panelsLayoutBody = $panelsLayout.find('.panelsLayout-body'),
-	$sidePanel = $panelsLayoutBody.find('.sidePanel'),
-	$mainPanel = $panelsLayoutBody.find('.mainPanel'),
-	filterButton = document.getElementById('button--openFilter'),
-	$tableMeta = $('.table-meta--main'),
-	win = window
-
-function setPanelsLayoutBodyHeight(){
-	var bodyOffsetTop = $panelsLayoutBody.offset().top
-	var winInnerHeight = win.innerHeight
-	var calculatedHeight = (winInnerHeight - bodyOffsetTop) + 'px'
-
-	if($('.filterPanel').length)
-		$('.filterPanel, .sidePanel, .mainPanel').css('height', calculatedHeight)
-	else
-		$('.sidePanel, .mainPanel').css('height', calculatedHeight)
-}
-
-setPanelsLayoutBodyHeight()
-$(win).resize(setPanelsLayoutBodyHeight)
-
-appl.def('open_side_panel', function(){
-	appl.def('is_showing_side_panel', true)
-	$panelsLayout.removeClass('is-showingFilterPanel')
-	$sidePanel.scrollTop(0)
-	$panelsLayout.addClass('is-showingSidePanel')
-	setPanelsLayoutBodyHeight()
-		$mainPanel.css({
-		left: '0px',
-		right: 'initial'
-	})
-	if (filterButton)
-		filterButton.removeAttribute('data-selected')
-	return appl
-})
-
-appl.def('close_side_panel', function(){
-	appl.def('is_showing_side_panel', false)
-	$mainPanel.find('tr').removeAttr('data-selected')
-	$panelsLayout.removeClass('is-showingSidePanel')
-	setPanelsLayoutBodyHeight()
-	window.history.pushState({},'index', win.location.pathname)
-	return appl
-})
-
-appl.def('open_filter_panel', function(){
-	$panelsLayout.removeClass('is-showingSidePanel')
-	$panelsLayout.addClass('is-showingFilterPanel')
-	$mainPanel.find('tr').removeAttr('data-selected')
-	$mainPanel.css({
-		right: '0px',
-		left: 'initial'
-	})
-	filterButton.setAttribute('data-selected', '')
-	window.history.pushState({},'index', win.location.pathname)
-	return appl
-})
-
-appl.def('close_filter_panel', function(){
-	$panelsLayout.removeClass('is-showingFilterPanel')
-	filterButton.removeAttribute('data-selected')
-	return appl
-})
-
-appl.def('scroll_main_panel', function(){
-	var main_panel = document.querySelector('.mainPanel')
-	main_panel.scrollTop = main_panel.scrollHeight
-})
-
diff --git a/client/js/common/pikaday-timepicker.js b/client/js/common/pikaday-timepicker.js
deleted file mode 100644
index 0e2cc62e..00000000
--- a/client/js/common/pikaday-timepicker.js
+++ /dev/null
@@ -1,30 +0,0 @@
-// License: LGPL-3.0-or-later
-const bind = require('attr-binder')
-const Pikaday = require('pikaday-time')
-const moment = require('moment')
-
-bind('pikaday-timepicker', function(container, format) {
-  const button = container.querySelector('a')
-  const input = container.querySelector('input')
-  input.readOnly = true
-
-  const maxDate_str = input.getAttribute('pikaday-maxDate')
-  const maxDate = maxDate_str ? moment(maxDate_str) : undefined
-  const defaultDate_str = input.getAttribute('pikaday-defaultDate')
-  const defaultDate = defaultDate_str ? moment(defaultDate_str) : undefined
-  new Pikaday({
-    showTime: true
-  , showMinutes: true
-  , showSeconds: false
-  , autoClose: false
-  , timeLabel: 'Time'
-  , format
-  , setDefaultDate: Boolean(defaultDate)
-  , field: input
-  , maxDate
-  , defaultDate
-  , trigger: button
-  })
-
-})
-
diff --git a/client/js/common/polyfills.js b/client/js/common/polyfills.js
deleted file mode 100644
index 9504f84e..00000000
--- a/client/js/common/polyfills.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: LGPL-3.0-or-later
-// Console fallback
-if (!window.console) {
-	window.console = new function() {
-		this.log = function(str) {}
-		this.dir = function(str) {}
-	}
-}
-
-// Promises polyfill
-require('es6-promise').polyfill()
diff --git a/client/js/common/post-form-data.es6 b/client/js/common/post-form-data.es6
deleted file mode 100644
index 6046b517..00000000
--- a/client/js/common/post-form-data.es6
+++ /dev/null
@@ -1,26 +0,0 @@
-// License: LGPL-3.0-or-later
-const flyd = require('flyd')
-const R = require('ramda')
-
-// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
-// Returns a flyd stream
-
-module.exports = R.curryN(2, (url, object) => {
-  var stream = flyd.stream()
-  var req = new XMLHttpRequest()
-  var formData = new FormData()
-  R.mapObjIndexed((val, key) => {
-    if(val.constructor === Object) val = JSON.stringify(val)
-    formData.append(key, val)
-  }, object)
-  req.open("POST", url)
-  // req.setRequestHeader('X-CSRF-Token', window._csrf)
-  req.send(formData)
-  req.onload = ev => {
-    var body = {}
-    try { body = JSON.parse(req.response) } catch(e) { }
-    stream( {status: req.status, body: body } )
-  }
-  return stream
-})
-
diff --git a/client/js/common/post-form-data.js b/client/js/common/post-form-data.js
deleted file mode 100644
index 3a95ce4c..00000000
--- a/client/js/common/post-form-data.js
+++ /dev/null
@@ -1,27 +0,0 @@
-// License: LGPL-3.0-or-later
-const flyd = require('flyd')
-const R = require('ramda')
-
-// TODO make this use flyd-ajax
-// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image
-// Returns a flyd stream
-
-module.exports = R.curryN(2, (url, object) => {
-  var stream = flyd.stream()
-  var req = new XMLHttpRequest()
-  var formData = new FormData()
-  R.mapObjIndexed((val, key) => {
-    if(val.constructor === Object) val = JSON.stringify(val)
-    formData.append(key, val)
-  }, object)
-  req.open("POST", url)
-  // req.setRequestHeader('X-CSRF-Token', window._csrf)
-  req.send(formData)
-  req.onload = ev => {
-    var body = {}
-    try { body = JSON.parse(req.response) } catch(e) { }
-    stream( {status: req.status, body: body } )
-  }
-  return stream
-})
-
diff --git a/client/js/common/request.js b/client/js/common/request.js
deleted file mode 100644
index ebd508ca..00000000
--- a/client/js/common/request.js
+++ /dev/null
@@ -1,11 +0,0 @@
-// License: LGPL-3.0-or-later
-const R = require('ramda')
-const request = require('flyd-ajax')
-
-module.exports = options => {
-  options.headers = R.merge({
-    'Content-Type': 'application/json'
-  , 'X-CSRF-Token': window._csrf
-  }, options.headers || {})
-  return request(options)
-}
diff --git a/client/js/common/restful_resource.js b/client/js/common/restful_resource.js
deleted file mode 100644
index 89029c3a..00000000
--- a/client/js/common/restful_resource.js
+++ /dev/null
@@ -1,139 +0,0 @@
-// License: LGPL-3.0-or-later
-/* A simple module for dealing with ajax-based resources in viewscript
-	*
-	*
-	* Define a 'resource object' in appl that has these properties
-	*   resource_name: 'donations' (plural name that matches the model)
-	*   path_prefix: '/' (optional, defaults to empty string, or relative path)
-	*   query: object of parameters to use for indexing (eg search queries)
-	*   after_action: function callback run after the request (where action is fetch, index, etc)
-	*   after_action_failure: callback for failed requests (where action is fetch, index, etc)
-	*
-	* Call the ajax functions like this:
-	* in js:
-	*   appl.ajax.index(appl.resource_object)
-	*   appl.ajax.create(appl.donations, {amount: 420})
-	* in viewscript in the dom:
-	*   ajax.index resource_object
-	*   ajax.create donations form_object
-	*/
-
-var request = require('../common/client')
-
-var restful_resource = {}
-module.exports = restful_resource
-
-
-appl.def('ajax', {
-	index: function(prop, node) {
-		var resource = appl.vs(prop) || {}
-		var name = resource.resource_name || prop
-		var path = resource.path_prefix || ''
-		before_request(prop)
-		return new Promise(function(resolve, reject) {
-			request.get(path + name).query(resource.query)
-				.end(function(err, resp) {
-					var tmp = resource.data
-					after_request(prop, err, resp)
-					if(resp.ok) {
-						if(resource.query && resource.query.page > 1 && resource.concat_data) {
-							appl.def(prop + '.data', tmp.concat(resp.body.data))
-						}
-						resolve(resp)
-					} else {
-						reject(resp)
-					}
-				})
-		})
-	},
-
-	fetch: function(prop, id, node) {
-		var resource = appl.vs(prop) || {}
-		var name = resource.resource_name || prop
-		var path = resource.path_prefix || ''
-		before_request(prop)
-		return new Promise(function(resolve, reject) {
-			request.get(path + name + '/' + id).query(resource.query)
-				.end(function(err, resp) {
-					after_request(prop, err, resp)
-					if(resp.ok) resolve(resp)
-					else reject(resp)
-				})
-		})
-	},
-
-	create: function(prop, form_obj, node) {
-		var resource = appl.vs(prop) || {}
-		var name = resource.resource_name || prop
-		var path = resource.path_prefix || ''
-		before_request(prop)
-		return new Promise(function(resolve, reject) {
-			request.post(path + name).send(nested_obj(name, form_obj))
-				.end(function(err, resp) {
-					after_request(prop, err, resp)
-					if(resp.ok) resolve(resp)
-					else reject(resp)
-				})
-		})
-	},
-
-	update: function(prop, id, form_obj, node) {
-		var resource = appl.vs(prop) || {}
-		var name = resource.resource_name || prop
-		var path = resource.path_prefix || ''
-		before_request(prop)
-		return new Promise(function(resolve, reject) {
-			request.put(path + name + '/' + id).send(nested_obj(name, form_obj))
-			.end(function(err, resp) {
-				after_request(prop, err, resp)
-				if(resp.ok) resolve(resp)
-				else reject(resp)
-			})
-		})
-	},
-
-	del: function(prop, id, node) {
-		var resource = appl.vs(prop) || {}
-		var path = (resource.path_prefix || '') + (resource.resource_name || prop)
-		before_request(prop)
-		return new Promise(function(resolve, reject) {
-			request.del(path + '/' + id)
-				.end(function(err, resp) {
-					after_request(prop, err, resp)
-					if(resp.ok) resolve(resp)
-					else reject(resp)
-				})
-		})
-	}
-})
-
-
-// Given a viewscript property, set some state before every request.
-// Eg. appl.ajax.index('donations') will cause appl.donations.loading to be
-// true before the request finishes
-function before_request(prop) {
-	appl.def(prop + '.loading', true)
-	appl.def(prop + '.error', '')
-}
-
-
-// Set some data after each request.
-function after_request(prop, err, resp) {
-	appl.def(prop + '.loading', false)
-	if(resp.ok) {
-		appl.def(prop, resp.body)
-	} else {
-		appl.def(prop + '.error', resp.body)
-	}
-}
-
-
-// Simply return an object nested under 'name'
-// Will singularize the given name if plural
-// eg: given 'donations' and {amount: 111}, return {donation: {amount: 111}}
-function nested_obj(name, child_obj) {
-	var parent_obj = {}
-	parent_obj[appl.to_singular(name)] = child_obj
-	return parent_obj
-}
-
diff --git a/client/js/common/sanitize-slug.js b/client/js/common/sanitize-slug.js
deleted file mode 100644
index e2cfcf4c..00000000
--- a/client/js/common/sanitize-slug.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// License: LGPL-3.0-or-later
-module.exports = str =>
- str.trim().toLowerCase()
-  .replace(/\s*[^A-Za-z0-9\-]\s*/g, '-') // Replace any oddballs with a hyphen
-  .replace(/-+$/g,'').replace(/^-+/, '').replace(/-+/, '-') // Remove starting/trailing and repeated hyphens
diff --git a/client/js/common/scroll_toggle_class.js b/client/js/common/scroll_toggle_class.js
deleted file mode 100644
index 7ed8a119..00000000
--- a/client/js/common/scroll_toggle_class.js
+++ /dev/null
@@ -1,32 +0,0 @@
-// License: LGPL-3.0-or-later
-module.exports = function(el, className, parentClass) {
-	var $el = $(el)
-	var elPxFromTop = $el.offset().top
-	var $parent = $el.parents(parentClass).length ? $el.parents(parentClass) : $el.parent()
-
-	var parentHeightPlusTop =  $parent.height() + $parent.offset().top - $el.height()
-	var $elToToggle
-
-	if (parentClass === undefined) {
-		parentClass = ''
-		$elToToggle = $el
-	} else {
-		$elToToggle = $parent
-	}
-
-
-	// the parentClass param is optional but if it is passed
-	// then the className is applied to it instead of the el
-
-	$(window).scroll(function() {
-		var scrollPosition = $(window).scrollTop()
-
-		if(scrollPosition >= elPxFromTop)
-			$elToToggle.addClass(className)
-		else
-			$elToToggle.removeClass(className)
-
-		if(parentClass && scrollPosition >= parentHeightPlusTop)
-			$elToToggle.removeClass(className)
-	})
-}
diff --git a/client/js/common/search-data.js b/client/js/common/search-data.js
deleted file mode 100644
index 1dcb383e..00000000
--- a/client/js/common/search-data.js
+++ /dev/null
@@ -1,52 +0,0 @@
-// License: LGPL-3.0-or-later
-const R = require('ramda')
-const h = require('flimflam/h')
-const flyd = require('flimflam/flyd')
-const getValidData = require('../common/get-valid-data')
-
-const getCurry = path => query => getValidData(path, query)
-
-module.exports = (path, pageLength) => {
-  const get = getCurry(path)
-  const searchLessQuery$ = flyd.stream()
-
-  const submitSearch$ = flyd.stream()
-  const searchQuery$ = flyd.map(searchQuery(pageLength), submitSearch$)
-
-  const searchLessResults$ = flyd.flatMap(q => get(q), searchLessQuery$)
-  const searchResults$ = flyd.flatMap(q => get(q), searchQuery$)
-
-  const allResults$ = flyd.merge(searchLessResults$, searchResults$)
-
-  const hasMoreResults$ = flyd.map(x => x && x.length >= pageLength, allResults$)
-
-  const data$ = flyd.scanMerge([
-    [searchLessResults$, (data, results) => R.concat(data, results)]
-  , [searchResults$, (data, results) => results]
-  ], [])
-
-  searchLessQuery$({page: 1, page_length: pageLength, search: ''})
-
-  const loading$ = flyd.mergeAll([
-    flyd.map(R.always(true), submitSearch$)
-  , flyd.map(R.always(true), searchLessQuery$)
-  , flyd.map(R.always(false), allResults$)
-  , flyd.stream(true)
-  ])
-
-  return {
-    data$
-  , searchLessQuery$
-  , loading$
-  , pageLength
-  , hasMoreResults$
-  , submitSearch$
-  }
-}
-
-const searchQuery = pageLength => ev => {
-  ev.preventDefault()
-  const search = ev.target.querySelector('input').value
-  return {page: 1, search, page_length: pageLength}
-}
-
diff --git a/client/js/common/super-agent-frp.js b/client/js/common/super-agent-frp.js
deleted file mode 100644
index 82968f83..00000000
--- a/client/js/common/super-agent-frp.js
+++ /dev/null
@@ -1,33 +0,0 @@
-// License: LGPL-3.0-or-later
-// super-agent with default json and csrf wrappers
-// Also has a FRP api (using flyd) rather than the default '.end'
-// Every call to .perform() returns a flyd stream
-
-var request = require('superagent')
-var flyd = require("flyd")
-
-var wrapper = {
-  post: function() {
-    return injectFlyd(request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
-  }
-, put: function() {
-    return injectFlyd(request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
-  }
-, del: function() {
-    return injectFlyd(request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json'))
-  }
-, get: function() {
-    return injectFlyd(request.get.apply(this, arguments).accept('json'))
-  }
-}
-
-function injectFlyd(req) {
-	req.perform = function() {
-		var $stream = flyd.stream()
-		req.end(function(err, resp) { $stream(resp) })
-		return $stream
-	}
-	return req
-}
-
-module.exports = wrapper
diff --git a/client/js/common/super-agent-promise.js b/client/js/common/super-agent-promise.js
deleted file mode 100644
index 12905091..00000000
--- a/client/js/common/super-agent-promise.js
+++ /dev/null
@@ -1,41 +0,0 @@
-// License: LGPL-3.0-or-later
-// super-agent with default json and csrf wrappers
-// Also has a Promise api ('.then' and '.catch') rather than the default '.end'
-
-var request = require('superagent')
-
-var wrapper = {}
-module.exports = wrapper
-
-wrapper.post = function() {
-	var req = request.post.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
-	return convert_to_promise(req)
-}
-
-wrapper.put = function() {
-	var req = request.put.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
-	return convert_to_promise(req)
-}
-
-wrapper.del = function() {
-	var req = request.del.apply(this, arguments).set('X-CSRF-Token', window._csrf).type('json')
-	return convert_to_promise(req)
-}
-
-wrapper.get = function(path) {
-	var req = request.get.call(this, path).accept('json')
-	return convert_to_promise(req)
-}
-
-function convert_to_promise(req) {
-	req.perform = function() {
-		return new Promise(function(resolve, reject) {
-			req.end(function(err, resp) {
-				if(resp && resp.ok) { resolve(resp) }
-				else { reject(resp) }
-			})
-		})
-	}
-	return req
-}
-
diff --git a/client/js/common/time-remaining.js b/client/js/common/time-remaining.js
deleted file mode 100644
index 8a435720..00000000
--- a/client/js/common/time-remaining.js
+++ /dev/null
@@ -1,39 +0,0 @@
-// License: LGPL-3.0-or-later
-const flyd = require('flyd')
-const flyd_every = require('flyd/module/every')
-const moment = require('moment-timezone')
-const format = require('../common/format')
-const pluralize = format.pluralize
-
-// Given an end dateTime ("2015-11-17 19:00") and a time-zone ("America/Los_Angeles"),
-// if the end dateTime has passed, return false
-// if the end dateTime is more than a day away
-//   then return the number of days away
-// if the end dateTime is less than a day away
-//   then return a countdown stream with seconds precision
-//
-// This function returns a stream.
-//
-// This function takes a timezone in the format "Country/City"
-// See here: http://momentjs.com/timezone/
-//
-
-
-const timeRemaining = (endDateTime, tz) => {
-  if(!endDateTime) return flyd.stream(false)
-  const format = "YYYY-MM-DD hh:mm:ss zz"
-  tz = tz || ENV.nonprofitTimezone || 'America/Los_Angeles'
-  const [now, end] = [moment().tz(tz), moment(endDateTime, format).tz(tz).seconds(59)]
-  console.log({now, end})
-  if(end.isBefore(now)) return flyd.stream(false)
-
-  if(end.diff(now, 'hours') <= 24) {
-    return flyd.map(
-      t => moment.utc(end.diff(moment(t))).format("HH:mm:ss")
-    , flyd_every(1000))
-  } else {
-    return flyd.stream(pluralize(end.diff(now, 'days'), 'days'))
-  }
-}
-
-module.exports = timeRemaining
diff --git a/client/js/common/utilities.js b/client/js/common/utilities.js
deleted file mode 100755
index 07fd38a9..00000000
--- a/client/js/common/utilities.js
+++ /dev/null
@@ -1,180 +0,0 @@
-// License: LGPL-3.0-or-later
-// Utilities!
-// XXX remove this whole file and split into modules with specific concerns
-const phoneFormatter = require('phone-formatter');
-const R = require('ramda')
-
-var utils = {}
-
-module.exports = utils
-
-// XXX remove
-utils.capitalize = string =>
-  string.charAt(0).toUpperCase() + string.slice(1)
-
-// Print a single message for Rails error responses
-// XXX remove
-utils.print_error = function (response) {
-	var msg = 'Sorry! We encountered an error.'
-	if(!response) return msg
-	if(response.status === 500) return msg
-	else if(response.status === 404) return "404 - Not found"
-	else if(response.status === 422 || response.status === 401) {
-		if(!response.responseJSON) return msg
-
-		var json = response.responseJSON
-		if(json.length) return json[0]
-
-		else if(json.errors)
-			for (var key in json.errors)
-				return key + ' ' + json.errors[key][0]
-
-		else if(json.error) return json.error
-
-		else return msg
-	}
-}
-
-// Retrieve a URL parameter
-// XXX remove
-utils.get_param = function(name) {
-	var param = decodeURI((RegExp(name + '=' + '(.+?)(&|$)').exec(location.search) || [null])[1])
-	return (param == 'undefined') ? undefined : param
-}
-
-// XXX remove
-utils.change_url_param = function(key, value) {
-	if (!history || !history.replaceState) return
-	history.replaceState({}, "", utils.update_param(key, value))
-}
-
-// XXX remove. Depended on only by 'change_url_param' above
-utils.update_param = function(key, value, url) {
-	if(!url) url = window.location.href
-	var re = new RegExp("([?&])" + key + "=.*?(&|#|$)(.*)", "gi")
-
-	if(re.test(url)) {
-		if(typeof value !== 'undefined' && value !== null)
-			return url.replace(re, '$1' + key + "=" + value + '$2$3')
-		else {
-			var hash = url.split('#')
-			url = hash[0].replace(re, '$1$3').replace(/(&|\?)$/, '')
-			if(typeof hash[1] !== 'undefined' && hash[1] !== null)
-				url += '#' + hash[1]
-			return url
-		}
-	} else {
-		if (typeof value !== 'undefined' && value !== null) {
-			var separator = url.indexOf('?') !== -1 ? '&' : '?',
-				hash = url.split('#')
-			url = hash[0] + separator + key + '=' + value
-			if(typeof hash[1] !== 'undefined' && hash[1] !== null)
-				url += '#' + hash[1]
-			return url
-		}
-		else return url
-	}
-}
-
-// Pad a number with leading zeros
-// XXX remove
-utils.zero_pad = function(num, size) {
-	var str = num + ""
-	while (str.length < size) str = "0" + str
-	return str
-}
-
-// for doing an action after the user pauses for a second after an event
-// XXX remove
-utils.delay = (function() {
-	var timer = 0
-	return function(ms, callback) {
-		clearTimeout(timer)
-		timer = setTimeout(callback, ms)
-	}
-})()
-
-utils.number_with_commas = function(n) {
-	return n.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
-}
-
-// Merge x's properties with y (mutating)
-utils.merge = function(x, y) {
-	for (var key in y) { x[key] = y[key]; }
-	return x
-}
-
-var format = require('./format')
-utils.dollars_to_cents = format.dollarsToCents
-utils.cents_to_dollars = format.centsToDollars
-
-// Create a single FormData object from any number of inputs and forms (not bound to a single form)
-// Kind of a re-implementation of: http://www.w3.org/html/wg/drafts/html/master/forms.html#constructing-the-form-data-set
-// XXX remove
-utils.toFormData = function(form_el) {
-	var form_data = new FormData()
-	$(form_el).find('input, select, textarea').each(function(index) {
-		if(!this.name) return
-		if(this.files && this.files[0])
-			form_data.append(this.name, this.files[0])
-		else if(this.getAttribute("type") === "checkbox")
-			form_data.append(this.name, this.checked)
-		else if(this.value)
-			form_data.append(this.name, this.value)
-	})
-	return form_data
-}
-
-utils.mergeFormData = function(formData, obj) {
-	for(var key in obj) formData.append(key, obj[key])
-	return formData
-}
-
-
-// Given an array of values, return an array only with unique values
-// XXX remove
-utils.uniq = function(arr) {
-	var obj = {}
-	var index
-	var len = arr.length
-	var result = [];
-	for(index = 0; index < len; index += 1) obj[arr[index]] = arr[index];
-	for(index in obj) result.push(obj[index]);
-	return result
-}
-
-// XXX remove
-utils.address_with_commas = function(street, city, state){
-	var address = [street, city, state]
-	var pretty_print_add = []
-	for(var i = 0; i < address.length; i += 1) {
-		if (address[i] !== '' && address[i] != null) pretty_print_add.push(address[i])
-	}
-	return pretty_print_add.join(', ')
-}
-
-utils.pretty_phone = function(phone){
-	if(!phone) {return false}
-  
-  // first remove any non-digit characters globally 
-  // and get length of phone number
-  var clean = String(phone).replace(/\D/g, '')
-  var len = clean.length
-
-  var format = "(NNN) NNN-NNNN"
-
-  // then format based on length
-  if(len === 10) {
-    return phoneFormatter.format(clean, format) 
-  }
-  if(len > 10) {
-    var first = clean.substring(0, len - 10)
-    var last10 = clean.substring(len - 10) 
-    return `+${first} ${phoneFormatter.format(last10, format)}`
-  }
-
-  // if number is less than 10, don't apply any formatting
-  // and just return it
-  return clean
-}
-
diff --git a/client/js/common/vendor/Chart.min.js b/client/js/common/vendor/Chart.min.js
deleted file mode 100755
index 27b08032..00000000
--- a/client/js/common/vendor/Chart.min.js
+++ /dev/null
@@ -1,3476 +0,0 @@
-// License: LGPL-3.0-or-later
-/*!
- * Chart.js
- * http://chartjs.org/
- * Version: 1.0.2
- *
- * Copyright 2015 Nick Downie
- * Released under the MIT license
- * https://github.com/nnnick/Chart.js/blob/master/LICENSE.md
- */
-
-
-(function(){
-
-	"use strict";
-
-	//Declare root variable - window in the browser, global on the server
-	var root = this;
-	var previous = root.Chart;
-
-	//Occupy the global variable of Chart, and create a simple base class
-	var Chart = function(context){
-		var chart = this;
-		this.canvas = context.canvas;
-
-		this.ctx = context;
-
-		//Variables global to the chart
-		var computeDimension = function(element,dimension)
-		{
-			if (element['offset'+dimension])
-			{
-				return element['offset'+dimension];
-			}
-			else
-			{
-				return document.defaultView.getComputedStyle(element).getPropertyValue(dimension);
-			}
-		}
-
-		var width = this.width = computeDimension(context.canvas,'Width');
-		var height = this.height = computeDimension(context.canvas,'Height');
-
-		// Firefox requires this to work correctly
-		context.canvas.width  = width;
-		context.canvas.height = height;
-
-		var width = this.width = context.canvas.width;
-		var height = this.height = context.canvas.height;
-		this.aspectRatio = this.width / this.height;
-		//High pixel density displays - multiply the size of the canvas height/width by the device pixel ratio, then scale.
-		helpers.retinaScale(this);
-
-		return this;
-	};
-	//Globally expose the defaults to allow for user updating/changing
-	Chart.defaults = {
-		global: {
-			// Boolean - Whether to animate the chart
-			animation: true,
-
-			// Number - Number of animation steps
-			animationSteps: 60,
-
-			// String - Animation easing effect
-			animationEasing: "easeOutQuart",
-
-			// Boolean - If we should show the scale at all
-			showScale: true,
-
-			// Boolean - If we want to override with a hard coded scale
-			scaleOverride: false,
-
-			// ** Required if scaleOverride is true **
-			// Number - The number of steps in a hard coded scale
-			scaleSteps: null,
-			// Number - The value jump in the hard coded scale
-			scaleStepWidth: null,
-			// Number - The scale starting value
-			scaleStartValue: null,
-
-			// String - Colour of the scale line
-			scaleLineColor: "rgba(0,0,0,.1)",
-
-			// Number - Pixel width of the scale line
-			scaleLineWidth: 1,
-
-			// Boolean - Whether to show labels on the scale
-			scaleShowLabels: true,
-
-			// Interpolated JS string - can access value
-			scaleLabel: "<%=value%>",
-
-			// Boolean - Whether the scale should stick to integers, and not show any floats even if drawing space is there
-			scaleIntegersOnly: true,
-
-			// Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
-			scaleBeginAtZero: false,
-
-			// String - Scale label font declaration for the scale label
-			scaleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
-
-			// Number - Scale label font size in pixels
-			scaleFontSize: 12,
-
-			// String - Scale label font weight style
-			scaleFontStyle: "normal",
-
-			// String - Scale label font colour
-			scaleFontColor: "#666",
-
-			// Boolean - whether or not the chart should be responsive and resize when the browser does.
-			responsive: false,
-
-			// Boolean - whether to maintain the starting aspect ratio or not when responsive, if set to false, will take up entire container
-			maintainAspectRatio: true,
-
-			// Boolean - Determines whether to draw tooltips on the canvas or not - attaches events to touchmove & mousemove
-			showTooltips: true,
-
-			// Boolean - Determines whether to draw built-in tooltip or call custom tooltip function
-			customTooltips: false,
-
-			// Array - Array of string names to attach tooltip events
-			tooltipEvents: ["mousemove", "touchstart", "touchmove", "mouseout"],
-
-			// String - Tooltip background colour
-			tooltipFillColor: "rgba(0,0,0,0.8)",
-
-			// String - Tooltip label font declaration for the scale label
-			tooltipFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
-
-			// Number - Tooltip label font size in pixels
-			tooltipFontSize: 14,
-
-			// String - Tooltip font weight style
-			tooltipFontStyle: "normal",
-
-			// String - Tooltip label font colour
-			tooltipFontColor: "#fff",
-
-			// String - Tooltip title font declaration for the scale label
-			tooltipTitleFontFamily: "'Helvetica Neue', 'Helvetica', 'Arial', sans-serif",
-
-			// Number - Tooltip title font size in pixels
-			tooltipTitleFontSize: 14,
-
-			// String - Tooltip title font weight style
-			tooltipTitleFontStyle: "bold",
-
-			// String - Tooltip title font colour
-			tooltipTitleFontColor: "#fff",
-
-			// Number - pixel width of padding around tooltip text
-			tooltipYPadding: 6,
-
-			// Number - pixel width of padding around tooltip text
-			tooltipXPadding: 6,
-
-			// Number - Size of the caret on the tooltip
-			tooltipCaretSize: 8,
-
-			// Number - Pixel radius of the tooltip border
-			tooltipCornerRadius: 6,
-
-			// Number - Pixel offset from point x to tooltip edge
-			tooltipXOffset: 10,
-
-			// String - Template string for single tooltips
-			tooltipTemplate: "<%if (label){%><%=label%>: <%}%><%= value %>",
-
-			// String - Template string for single tooltips
-			multiTooltipTemplate: "<%= value %>",
-
-			// String - Colour behind the legend colour block
-			multiTooltipKeyBackground: '#fff',
-
-			// Function - Will fire on animation progression.
-			onAnimationProgress: function(){},
-
-			// Function - Will fire on animation completion.
-			onAnimationComplete: function(){}
-
-		}
-	};
-
-	//Create a dictionary of chart types, to allow for extension of existing types
-	Chart.types = {};
-
-	//Global Chart helpers object for utility methods and classes
-	var helpers = Chart.helpers = {};
-
-		//-- Basic js utility methods
-	var each = helpers.each = function(loopable,callback,self){
-			var additionalArgs = Array.prototype.slice.call(arguments, 3);
-			// Check to see if null or undefined firstly.
-			if (loopable){
-				if (loopable.length === +loopable.length){
-					var i;
-					for (i=0; i= 0; i--) {
-				var currentItem = arrayToSearch[i];
-				if (filterCallback(currentItem)){
-					return currentItem;
-				}
-			}
-		},
-		inherits = helpers.inherits = function(extensions){
-			//Basic javascript inheritance based on the model created in Backbone.js
-			var parent = this;
-			var ChartElement = (extensions && extensions.hasOwnProperty("constructor")) ? extensions.constructor : function(){ return parent.apply(this, arguments); };
-
-			var Surrogate = function(){ this.constructor = ChartElement;};
-			Surrogate.prototype = parent.prototype;
-			ChartElement.prototype = new Surrogate();
-
-			ChartElement.extend = inherits;
-
-			if (extensions) extend(ChartElement.prototype, extensions);
-
-			ChartElement.__super__ = parent.prototype;
-
-			return ChartElement;
-		},
-		noop = helpers.noop = function(){},
-		uid = helpers.uid = (function(){
-			var id=0;
-			return function(){
-				return "chart-" + id++;
-			};
-		})(),
-		warn = helpers.warn = function(str){
-			//Method for warning of errors
-			if (window.console && typeof window.console.warn == "function") console.warn(str);
-		},
-		amd = helpers.amd = (typeof define == 'function' && define.amd),
-		//-- Math methods
-		isNumber = helpers.isNumber = function(n){
-			return !isNaN(parseFloat(n)) && isFinite(n);
-		},
-		max = helpers.max = function(array){
-			return Math.max.apply( Math, array );
-		},
-		min = helpers.min = function(array){
-			return Math.min.apply( Math, array );
-		},
-		cap = helpers.cap = function(valueToCap,maxValue,minValue){
-			if(isNumber(maxValue)) {
-				if( valueToCap > maxValue ) {
-					return maxValue;
-				}
-			}
-			else if(isNumber(minValue)){
-				if ( valueToCap < minValue ){
-					return minValue;
-				}
-			}
-			return valueToCap;
-		},
-		getDecimalPlaces = helpers.getDecimalPlaces = function(num){
-			if (num%1!==0 && isNumber(num)){
-				return num.toString().split(".")[1].length;
-			}
-			else {
-				return 0;
-			}
-		},
-		toRadians = helpers.radians = function(degrees){
-			return degrees * (Math.PI/180);
-		},
-		// Gets the angle from vertical upright to the point about a centre.
-		getAngleFromPoint = helpers.getAngleFromPoint = function(centrePoint, anglePoint){
-			var distanceFromXCenter = anglePoint.x - centrePoint.x,
-				distanceFromYCenter = anglePoint.y - centrePoint.y,
-				radialDistanceFromCenter = Math.sqrt( distanceFromXCenter * distanceFromXCenter + distanceFromYCenter * distanceFromYCenter);
-
-
-			var angle = Math.PI * 2 + Math.atan2(distanceFromYCenter, distanceFromXCenter);
-
-			//If the segment is in the top left quadrant, we need to add another rotation to the angle
-			if (distanceFromXCenter < 0 && distanceFromYCenter < 0){
-				angle += Math.PI*2;
-			}
-
-			return {
-				angle: angle,
-				distance: radialDistanceFromCenter
-			};
-		},
-		aliasPixel = helpers.aliasPixel = function(pixelWidth){
-			return (pixelWidth % 2 === 0) ? 0 : 0.5;
-		},
-		splineCurve = helpers.splineCurve = function(FirstPoint,MiddlePoint,AfterPoint,t){
-			//Props to Rob Spencer at scaled innovation for his post on splining between points
-			//http://scaledinnovation.com/analytics/splines/aboutSplines.html
-			var d01=Math.sqrt(Math.pow(MiddlePoint.x-FirstPoint.x,2)+Math.pow(MiddlePoint.y-FirstPoint.y,2)),
-				d12=Math.sqrt(Math.pow(AfterPoint.x-MiddlePoint.x,2)+Math.pow(AfterPoint.y-MiddlePoint.y,2)),
-				fa=t*d01/(d01+d12),// scaling factor for triangle Ta
-				fb=t*d12/(d01+d12);
-			return {
-				inner : {
-					x : MiddlePoint.x-fa*(AfterPoint.x-FirstPoint.x),
-					y : MiddlePoint.y-fa*(AfterPoint.y-FirstPoint.y)
-				},
-				outer : {
-					x: MiddlePoint.x+fb*(AfterPoint.x-FirstPoint.x),
-					y : MiddlePoint.y+fb*(AfterPoint.y-FirstPoint.y)
-				}
-			};
-		},
-		calculateOrderOfMagnitude = helpers.calculateOrderOfMagnitude = function(val){
-			return Math.floor(Math.log(val) / Math.LN10);
-		},
-		calculateScaleRange = helpers.calculateScaleRange = function(valuesArray, drawingSize, textSize, startFromZero, integersOnly){
-
-			//Set a minimum step of two - a point at the top of the graph, and a point at the base
-			var minSteps = 2,
-				maxSteps = Math.floor(drawingSize/(textSize * 1.5)),
-				skipFitting = (minSteps >= maxSteps);
-
-			var maxValue = max(valuesArray),
-				minValue = min(valuesArray);
-
-			// We need some degree of seperation here to calculate the scales if all the values are the same
-			// Adding/minusing 0.5 will give us a range of 1.
-			if (maxValue === minValue){
-				maxValue += 0.5;
-				// So we don't end up with a graph with a negative start value if we've said always start from zero
-				if (minValue >= 0.5 && !startFromZero){
-					minValue -= 0.5;
-				}
-				else{
-					// Make up a whole number above the values
-					maxValue += 0.5;
-				}
-			}
-
-			var	valueRange = Math.abs(maxValue - minValue),
-				rangeOrderOfMagnitude = calculateOrderOfMagnitude(valueRange),
-				graphMax = Math.ceil(maxValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
-				graphMin = (startFromZero) ? 0 : Math.floor(minValue / (1 * Math.pow(10, rangeOrderOfMagnitude))) * Math.pow(10, rangeOrderOfMagnitude),
-				graphRange = graphMax - graphMin,
-				stepValue = Math.pow(10, rangeOrderOfMagnitude),
-				numberOfSteps = Math.round(graphRange / stepValue);
-
-			//If we have more space on the graph we'll use it to give more definition to the data
-			while((numberOfSteps > maxSteps || (numberOfSteps * 2) < maxSteps) && !skipFitting) {
-				if(numberOfSteps > maxSteps){
-					stepValue *=2;
-					numberOfSteps = Math.round(graphRange/stepValue);
-					// Don't ever deal with a decimal number of steps - cancel fitting and just use the minimum number of steps.
-					if (numberOfSteps % 1 !== 0){
-						skipFitting = true;
-					}
-				}
-				//We can fit in double the amount of scale points on the scale
-				else{
-					//If user has declared ints only, and the step value isn't a decimal
-					if (integersOnly && rangeOrderOfMagnitude >= 0){
-						//If the user has said integers only, we need to check that making the scale more granular wouldn't make it a float
-						if(stepValue/2 % 1 === 0){
-							stepValue /=2;
-							numberOfSteps = Math.round(graphRange/stepValue);
-						}
-						//If it would make it a float break out of the loop
-						else{
-							break;
-						}
-					}
-					//If the scale doesn't have to be an int, make the scale more granular anyway.
-					else{
-						stepValue /=2;
-						numberOfSteps = Math.round(graphRange/stepValue);
-					}
-
-				}
-			}
-
-			if (skipFitting){
-				numberOfSteps = minSteps;
-				stepValue = graphRange / numberOfSteps;
-			}
-
-			return {
-				steps : numberOfSteps,
-				stepValue : stepValue,
-				min : graphMin,
-				max	: graphMin + (numberOfSteps * stepValue)
-			};
-
-		},
-		/* jshint ignore:start */
-		// Blows up jshint errors based on the new Function constructor
-		//Templating methods
-		//Javascript micro templating by John Resig - source at http://ejohn.org/blog/javascript-micro-templating/
-		template = helpers.template = function(templateString, valuesObject){
-
-			// If templateString is function rather than string-template - call the function for valuesObject
-
-			if(templateString instanceof Function){
-			 	return templateString(valuesObject);
-		 	}
-
-			var cache = {};
-			function tmpl(str, data){
-				// Figure out if we're getting a template, or if we need to
-				// load the template - and be sure to cache the result.
-				var fn = !/\W/.test(str) ?
-				cache[str] = cache[str] :
-
-				// Generate a reusable function that will serve as a template
-				// generator (and which will be cached).
-				new Function("obj",
-					"var p=[],print=function(){p.push.apply(p,arguments);};" +
-
-					// Introduce the data as local variables using with(){}
-					"with(obj){p.push('" +
-
-					// Convert the template into pure JavaScript
-					str
-						.replace(/[\r\t\n]/g, " ")
-						.split("<%").join("\t")
-						.replace(/((^|%>)[^\t]*)'/g, "$1\r")
-						.replace(/\t=(.*?)%>/g, "',$1,'")
-						.split("\t").join("');")
-						.split("%>").join("p.push('")
-						.split("\r").join("\\'") +
-					"');}return p.join('');"
-				);
-
-				// Provide some basic currying to the user
-				return data ? fn( data ) : fn;
-			}
-			return tmpl(templateString,valuesObject);
-		},
-		/* jshint ignore:end */
-		generateLabels = helpers.generateLabels = function(templateString,numberOfSteps,graphMin,stepValue){
-			var labelsArray = new Array(numberOfSteps);
-			if (labelTemplateString){
-				each(labelsArray,function(val,index){
-					labelsArray[index] = template(templateString,{value: (graphMin + (stepValue*(index+1)))});
-				});
-			}
-			return labelsArray;
-		},
-		//--Animation methods
-		//Easing functions adapted from Robert Penner's easing equations
-		//http://www.robertpenner.com/easing/
-		easingEffects = helpers.easingEffects = {
-			linear: function (t) {
-				return t;
-			},
-			easeInQuad: function (t) {
-				return t * t;
-			},
-			easeOutQuad: function (t) {
-				return -1 * t * (t - 2);
-			},
-			easeInOutQuad: function (t) {
-				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t;
-				return -1 / 2 * ((--t) * (t - 2) - 1);
-			},
-			easeInCubic: function (t) {
-				return t * t * t;
-			},
-			easeOutCubic: function (t) {
-				return 1 * ((t = t / 1 - 1) * t * t + 1);
-			},
-			easeInOutCubic: function (t) {
-				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t;
-				return 1 / 2 * ((t -= 2) * t * t + 2);
-			},
-			easeInQuart: function (t) {
-				return t * t * t * t;
-			},
-			easeOutQuart: function (t) {
-				return -1 * ((t = t / 1 - 1) * t * t * t - 1);
-			},
-			easeInOutQuart: function (t) {
-				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t;
-				return -1 / 2 * ((t -= 2) * t * t * t - 2);
-			},
-			easeInQuint: function (t) {
-				return 1 * (t /= 1) * t * t * t * t;
-			},
-			easeOutQuint: function (t) {
-				return 1 * ((t = t / 1 - 1) * t * t * t * t + 1);
-			},
-			easeInOutQuint: function (t) {
-				if ((t /= 1 / 2) < 1) return 1 / 2 * t * t * t * t * t;
-				return 1 / 2 * ((t -= 2) * t * t * t * t + 2);
-			},
-			easeInSine: function (t) {
-				return -1 * Math.cos(t / 1 * (Math.PI / 2)) + 1;
-			},
-			easeOutSine: function (t) {
-				return 1 * Math.sin(t / 1 * (Math.PI / 2));
-			},
-			easeInOutSine: function (t) {
-				return -1 / 2 * (Math.cos(Math.PI * t / 1) - 1);
-			},
-			easeInExpo: function (t) {
-				return (t === 0) ? 1 : 1 * Math.pow(2, 10 * (t / 1 - 1));
-			},
-			easeOutExpo: function (t) {
-				return (t === 1) ? 1 : 1 * (-Math.pow(2, -10 * t / 1) + 1);
-			},
-			easeInOutExpo: function (t) {
-				if (t === 0) return 0;
-				if (t === 1) return 1;
-				if ((t /= 1 / 2) < 1) return 1 / 2 * Math.pow(2, 10 * (t - 1));
-				return 1 / 2 * (-Math.pow(2, -10 * --t) + 2);
-			},
-			easeInCirc: function (t) {
-				if (t >= 1) return t;
-				return -1 * (Math.sqrt(1 - (t /= 1) * t) - 1);
-			},
-			easeOutCirc: function (t) {
-				return 1 * Math.sqrt(1 - (t = t / 1 - 1) * t);
-			},
-			easeInOutCirc: function (t) {
-				if ((t /= 1 / 2) < 1) return -1 / 2 * (Math.sqrt(1 - t * t) - 1);
-				return 1 / 2 * (Math.sqrt(1 - (t -= 2) * t) + 1);
-			},
-			easeInElastic: function (t) {
-				var s = 1.70158;
-				var p = 0;
-				var a = 1;
-				if (t === 0) return 0;
-				if ((t /= 1) == 1) return 1;
-				if (!p) p = 1 * 0.3;
-				if (a < Math.abs(1)) {
-					a = 1;
-					s = p / 4;
-				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
-				return -(a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
-			},
-			easeOutElastic: function (t) {
-				var s = 1.70158;
-				var p = 0;
-				var a = 1;
-				if (t === 0) return 0;
-				if ((t /= 1) == 1) return 1;
-				if (!p) p = 1 * 0.3;
-				if (a < Math.abs(1)) {
-					a = 1;
-					s = p / 4;
-				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
-				return a * Math.pow(2, -10 * t) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) + 1;
-			},
-			easeInOutElastic: function (t) {
-				var s = 1.70158;
-				var p = 0;
-				var a = 1;
-				if (t === 0) return 0;
-				if ((t /= 1 / 2) == 2) return 1;
-				if (!p) p = 1 * (0.3 * 1.5);
-				if (a < Math.abs(1)) {
-					a = 1;
-					s = p / 4;
-				} else s = p / (2 * Math.PI) * Math.asin(1 / a);
-				if (t < 1) return -0.5 * (a * Math.pow(2, 10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p));
-				return a * Math.pow(2, -10 * (t -= 1)) * Math.sin((t * 1 - s) * (2 * Math.PI) / p) * 0.5 + 1;
-			},
-			easeInBack: function (t) {
-				var s = 1.70158;
-				return 1 * (t /= 1) * t * ((s + 1) * t - s);
-			},
-			easeOutBack: function (t) {
-				var s = 1.70158;
-				return 1 * ((t = t / 1 - 1) * t * ((s + 1) * t + s) + 1);
-			},
-			easeInOutBack: function (t) {
-				var s = 1.70158;
-				if ((t /= 1 / 2) < 1) return 1 / 2 * (t * t * (((s *= (1.525)) + 1) * t - s));
-				return 1 / 2 * ((t -= 2) * t * (((s *= (1.525)) + 1) * t + s) + 2);
-			},
-			easeInBounce: function (t) {
-				return 1 - easingEffects.easeOutBounce(1 - t);
-			},
-			easeOutBounce: function (t) {
-				if ((t /= 1) < (1 / 2.75)) {
-					return 1 * (7.5625 * t * t);
-				} else if (t < (2 / 2.75)) {
-					return 1 * (7.5625 * (t -= (1.5 / 2.75)) * t + 0.75);
-				} else if (t < (2.5 / 2.75)) {
-					return 1 * (7.5625 * (t -= (2.25 / 2.75)) * t + 0.9375);
-				} else {
-					return 1 * (7.5625 * (t -= (2.625 / 2.75)) * t + 0.984375);
-				}
-			},
-			easeInOutBounce: function (t) {
-				if (t < 1 / 2) return easingEffects.easeInBounce(t * 2) * 0.5;
-				return easingEffects.easeOutBounce(t * 2 - 1) * 0.5 + 1 * 0.5;
-			}
-		},
-		//Request animation polyfill - http://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
-		requestAnimFrame = helpers.requestAnimFrame = (function(){
-			return window.requestAnimationFrame ||
-				window.webkitRequestAnimationFrame ||
-				window.mozRequestAnimationFrame ||
-				window.oRequestAnimationFrame ||
-				window.msRequestAnimationFrame ||
-				function(callback) {
-					return window.setTimeout(callback, 1000 / 60);
-				};
-		})(),
-		cancelAnimFrame = helpers.cancelAnimFrame = (function(){
-			return window.cancelAnimationFrame ||
-				window.webkitCancelAnimationFrame ||
-				window.mozCancelAnimationFrame ||
-				window.oCancelAnimationFrame ||
-				window.msCancelAnimationFrame ||
-				function(callback) {
-					return window.clearTimeout(callback, 1000 / 60);
-				};
-		})(),
-		animationLoop = helpers.animationLoop = function(callback,totalSteps,easingString,onProgress,onComplete,chartInstance){
-
-			var currentStep = 0,
-				easingFunction = easingEffects[easingString] || easingEffects.linear;
-
-			var animationFrame = function(){
-				currentStep++;
-				var stepDecimal = currentStep/totalSteps;
-				var easeDecimal = easingFunction(stepDecimal);
-
-				callback.call(chartInstance,easeDecimal,stepDecimal, currentStep);
-				onProgress.call(chartInstance,easeDecimal,stepDecimal);
-				if (currentStep < totalSteps){
-					chartInstance.animationFrame = requestAnimFrame(animationFrame);
-				} else{
-					onComplete.apply(chartInstance);
-				}
-			};
-			requestAnimFrame(animationFrame);
-		},
-		//-- DOM methods
-		getRelativePosition = helpers.getRelativePosition = function(evt){
-			var mouseX, mouseY;
-			var e = evt.originalEvent || evt,
-				canvas = evt.currentTarget || evt.srcElement,
-				boundingRect = canvas.getBoundingClientRect();
-
-			if (e.touches){
-				mouseX = e.touches[0].clientX - boundingRect.left;
-				mouseY = e.touches[0].clientY - boundingRect.top;
-
-			}
-			else{
-				mouseX = e.clientX - boundingRect.left;
-				mouseY = e.clientY - boundingRect.top;
-			}
-
-			return {
-				x : mouseX,
-				y : mouseY
-			};
-
-		},
-		addEvent = helpers.addEvent = function(node,eventType,method){
-			if (node.addEventListener){
-				node.addEventListener(eventType,method);
-			} else if (node.attachEvent){
-				node.attachEvent("on"+eventType, method);
-			} else {
-				node["on"+eventType] = method;
-			}
-		},
-		removeEvent = helpers.removeEvent = function(node, eventType, handler){
-			if (node.removeEventListener){
-				node.removeEventListener(eventType, handler, false);
-			} else if (node.detachEvent){
-				node.detachEvent("on"+eventType,handler);
-			} else{
-				node["on" + eventType] = noop;
-			}
-		},
-		bindEvents = helpers.bindEvents = function(chartInstance, arrayOfEvents, handler){
-			// Create the events object if it's not already present
-			if (!chartInstance.events) chartInstance.events = {};
-
-			each(arrayOfEvents,function(eventName){
-				chartInstance.events[eventName] = function(){
-					handler.apply(chartInstance, arguments);
-				};
-				addEvent(chartInstance.chart.canvas,eventName,chartInstance.events[eventName]);
-			});
-		},
-		unbindEvents = helpers.unbindEvents = function (chartInstance, arrayOfEvents) {
-			each(arrayOfEvents, function(handler,eventName){
-				removeEvent(chartInstance.chart.canvas, eventName, handler);
-			});
-		},
-		getMaximumWidth = helpers.getMaximumWidth = function(domNode){
-			var container = domNode.parentNode;
-			// TODO = check cross browser stuff with this.
-			return container.clientWidth;
-		},
-		getMaximumHeight = helpers.getMaximumHeight = function(domNode){
-			var container = domNode.parentNode;
-			// TODO = check cross browser stuff with this.
-			return container.clientHeight;
-		},
-		getMaximumSize = helpers.getMaximumSize = helpers.getMaximumWidth, // legacy support
-		retinaScale = helpers.retinaScale = function(chart){
-			var ctx = chart.ctx,
-				width = chart.canvas.width,
-				height = chart.canvas.height;
-
-			if (window.devicePixelRatio) {
-				ctx.canvas.style.width = width + "px";
-				ctx.canvas.style.height = height + "px";
-				ctx.canvas.height = height * window.devicePixelRatio;
-				ctx.canvas.width = width * window.devicePixelRatio;
-				ctx.scale(window.devicePixelRatio, window.devicePixelRatio);
-			}
-		},
-		//-- Canvas methods
-		clear = helpers.clear = function(chart){
-			chart.ctx.clearRect(0,0,chart.width,chart.height);
-		},
-		fontString = helpers.fontString = function(pixelSize,fontStyle,fontFamily){
-			return fontStyle + " " + pixelSize+"px " + fontFamily;
-		},
-		longestText = helpers.longestText = function(ctx,font,arrayOfStrings){
-			ctx.font = font;
-			var longest = 0;
-			each(arrayOfStrings,function(string){
-				var textWidth = ctx.measureText(string).width;
-				longest = (textWidth > longest) ? textWidth : longest;
-			});
-			return longest;
-		},
-		drawRoundedRectangle = helpers.drawRoundedRectangle = function(ctx,x,y,width,height,radius){
-			ctx.beginPath();
-			ctx.moveTo(x + radius, y);
-			ctx.lineTo(x + width - radius, y);
-			ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
-			ctx.lineTo(x + width, y + height - radius);
-			ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
-			ctx.lineTo(x + radius, y + height);
-			ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
-			ctx.lineTo(x, y + radius);
-			ctx.quadraticCurveTo(x, y, x + radius, y);
-			ctx.closePath();
-		};
-
-
-	//Store a reference to each instance - allowing us to globally resize chart instances on window resize.
-	//Destroy method on the chart will remove the instance of the chart from this reference.
-	Chart.instances = {};
-
-	Chart.Type = function(data,options,chart){
-		this.options = options;
-		this.chart = chart;
-		this.id = uid();
-		//Add the chart instance to the global namespace
-		Chart.instances[this.id] = this;
-
-		// Initialize is always called when a chart type is created
-		// By default it is a no op, but it should be extended
-		if (options.responsive){
-			this.resize();
-		}
-		this.initialize.call(this,data);
-	};
-
-	//Core methods that'll be a part of every chart type
-	extend(Chart.Type.prototype,{
-		initialize : function(){return this;},
-		clear : function(){
-			clear(this.chart);
-			return this;
-		},
-		stop : function(){
-			// Stops any current animation loop occuring
-			cancelAnimFrame(this.animationFrame);
-			return this;
-		},
-		resize : function(callback){
-			this.stop();
-			var canvas = this.chart.canvas,
-				newWidth = getMaximumWidth(this.chart.canvas),
-				newHeight = this.options.maintainAspectRatio ? newWidth / this.chart.aspectRatio : getMaximumHeight(this.chart.canvas);
-
-			canvas.width = this.chart.width = newWidth;
-			canvas.height = this.chart.height = newHeight;
-
-			retinaScale(this.chart);
-
-			if (typeof callback === "function"){
-				callback.apply(this, Array.prototype.slice.call(arguments, 1));
-			}
-			return this;
-		},
-		reflow : noop,
-		render : function(reflow){
-			if (reflow){
-				this.reflow();
-			}
-			if (this.options.animation && !reflow){
-				helpers.animationLoop(
-					this.draw,
-					this.options.animationSteps,
-					this.options.animationEasing,
-					this.options.onAnimationProgress,
-					this.options.onAnimationComplete,
-					this
-				);
-			}
-			else{
-				this.draw();
-				this.options.onAnimationComplete.call(this);
-			}
-			return this;
-		},
-		generateLegend : function(){
-			return template(this.options.legendTemplate,this);
-		},
-		destroy : function(){
-			this.clear();
-			unbindEvents(this, this.events);
-			var canvas = this.chart.canvas;
-
-			// Reset canvas height/width attributes starts a fresh with the canvas context
-			canvas.width = this.chart.width;
-			canvas.height = this.chart.height;
-
-			// < IE9 doesn't support removeProperty
-			if (canvas.style.removeProperty) {
-				canvas.style.removeProperty('width');
-				canvas.style.removeProperty('height');
-			} else {
-				canvas.style.removeAttribute('width');
-				canvas.style.removeAttribute('height');
-			}
-
-			delete Chart.instances[this.id];
-		},
-		showTooltip : function(ChartElements, forceRedraw){
-			// Only redraw the chart if we've actually changed what we're hovering on.
-			if (typeof this.activeElements === 'undefined') this.activeElements = [];
-
-			var isChanged = (function(Elements){
-				var changed = false;
-
-				if (Elements.length !== this.activeElements.length){
-					changed = true;
-					return changed;
-				}
-
-				each(Elements, function(element, index){
-					if (element !== this.activeElements[index]){
-						changed = true;
-					}
-				}, this);
-				return changed;
-			}).call(this, ChartElements);
-
-			if (!isChanged && !forceRedraw){
-				return;
-			}
-			else{
-				this.activeElements = ChartElements;
-			}
-			this.draw();
-			if(this.options.customTooltips){
-				this.options.customTooltips(false);
-			}
-			if (ChartElements.length > 0){
-				// If we have multiple datasets, show a MultiTooltip for all of the data points at that index
-				if (this.datasets && this.datasets.length > 1) {
-					var dataArray,
-						dataIndex;
-
-					for (var i = this.datasets.length - 1; i >= 0; i--) {
-						dataArray = this.datasets[i].points || this.datasets[i].bars || this.datasets[i].segments;
-						dataIndex = indexOf(dataArray, ChartElements[0]);
-						if (dataIndex !== -1){
-							break;
-						}
-					}
-					var tooltipLabels = [],
-						tooltipColors = [],
-						medianPosition = (function(index) {
-
-							// Get all the points at that particular index
-							var Elements = [],
-								dataCollection,
-								xPositions = [],
-								yPositions = [],
-								xMax,
-								yMax,
-								xMin,
-								yMin;
-							helpers.each(this.datasets, function(dataset){
-								dataCollection = dataset.points || dataset.bars || dataset.segments;
-								if (dataCollection[dataIndex] && dataCollection[dataIndex].hasValue()){
-									Elements.push(dataCollection[dataIndex]);
-								}
-							});
-
-							helpers.each(Elements, function(element) {
-								xPositions.push(element.x);
-								yPositions.push(element.y);
-
-
-								//Include any colour information about the element
-								tooltipLabels.push(helpers.template(this.options.multiTooltipTemplate, element));
-								tooltipColors.push({
-									fill: element._saved.fillColor || element.fillColor,
-									stroke: element._saved.strokeColor || element.strokeColor
-								});
-
-							}, this);
-
-							yMin = min(yPositions);
-							yMax = max(yPositions);
-
-							xMin = min(xPositions);
-							xMax = max(xPositions);
-
-							return {
-								x: (xMin > this.chart.width/2) ? xMin : xMax,
-								y: (yMin + yMax)/2
-							};
-						}).call(this, dataIndex);
-
-					new Chart.MultiTooltip({
-						x: medianPosition.x,
-						y: medianPosition.y,
-						xPadding: this.options.tooltipXPadding,
-						yPadding: this.options.tooltipYPadding,
-						xOffset: this.options.tooltipXOffset,
-						fillColor: this.options.tooltipFillColor,
-						textColor: this.options.tooltipFontColor,
-						fontFamily: this.options.tooltipFontFamily,
-						fontStyle: this.options.tooltipFontStyle,
-						fontSize: this.options.tooltipFontSize,
-						titleTextColor: this.options.tooltipTitleFontColor,
-						titleFontFamily: this.options.tooltipTitleFontFamily,
-						titleFontStyle: this.options.tooltipTitleFontStyle,
-						titleFontSize: this.options.tooltipTitleFontSize,
-						cornerRadius: this.options.tooltipCornerRadius,
-						labels: tooltipLabels,
-						legendColors: tooltipColors,
-						legendColorBackground : this.options.multiTooltipKeyBackground,
-						title: ChartElements[0].label,
-						chart: this.chart,
-						ctx: this.chart.ctx,
-						custom: this.options.customTooltips
-					}).draw();
-
-				} else {
-					each(ChartElements, function(Element) {
-						var tooltipPosition = Element.tooltipPosition();
-						new Chart.Tooltip({
-							x: Math.round(tooltipPosition.x),
-							y: Math.round(tooltipPosition.y),
-							xPadding: this.options.tooltipXPadding,
-							yPadding: this.options.tooltipYPadding,
-							fillColor: this.options.tooltipFillColor,
-							textColor: this.options.tooltipFontColor,
-							fontFamily: this.options.tooltipFontFamily,
-							fontStyle: this.options.tooltipFontStyle,
-							fontSize: this.options.tooltipFontSize,
-							caretHeight: this.options.tooltipCaretSize,
-							cornerRadius: this.options.tooltipCornerRadius,
-							text: template(this.options.tooltipTemplate, Element),
-							chart: this.chart,
-							custom: this.options.customTooltips
-						}).draw();
-					}, this);
-				}
-			}
-			return this;
-		},
-		toBase64Image : function(){
-			return this.chart.canvas.toDataURL.apply(this.chart.canvas, arguments);
-		}
-	});
-
-	Chart.Type.extend = function(extensions){
-
-		var parent = this;
-
-		var ChartType = function(){
-			return parent.apply(this,arguments);
-		};
-
-		//Copy the prototype object of the this class
-		ChartType.prototype = clone(parent.prototype);
-		//Now overwrite some of the properties in the base class with the new extensions
-		extend(ChartType.prototype, extensions);
-
-		ChartType.extend = Chart.Type.extend;
-
-		if (extensions.name || parent.prototype.name){
-
-			var chartName = extensions.name || parent.prototype.name;
-			//Assign any potential default values of the new chart type
-
-			//If none are defined, we'll use a clone of the chart type this is being extended from.
-			//I.e. if we extend a line chart, we'll use the defaults from the line chart if our new chart
-			//doesn't define some defaults of their own.
-
-			var baseDefaults = (Chart.defaults[parent.prototype.name]) ? clone(Chart.defaults[parent.prototype.name]) : {};
-
-			Chart.defaults[chartName] = extend(baseDefaults,extensions.defaults);
-
-			Chart.types[chartName] = ChartType;
-
-			//Register this new chart type in the Chart prototype
-			Chart.prototype[chartName] = function(data,options){
-				var config = merge(Chart.defaults.global, Chart.defaults[chartName], options || {});
-				return new ChartType(data,config,this);
-			};
-		} else{
-			warn("Name not provided for this chart, so it hasn't been registered");
-		}
-		return parent;
-	};
-
-	Chart.Element = function(configuration){
-		extend(this,configuration);
-		this.initialize.apply(this,arguments);
-		this.save();
-	};
-	extend(Chart.Element.prototype,{
-		initialize : function(){},
-		restore : function(props){
-			if (!props){
-				extend(this,this._saved);
-			} else {
-				each(props,function(key){
-					this[key] = this._saved[key];
-				},this);
-			}
-			return this;
-		},
-		save : function(){
-			this._saved = clone(this);
-			delete this._saved._saved;
-			return this;
-		},
-		update : function(newProps){
-			each(newProps,function(value,key){
-				this._saved[key] = this[key];
-				this[key] = value;
-			},this);
-			return this;
-		},
-		transition : function(props,ease){
-			each(props,function(value,key){
-				this[key] = ((value - this._saved[key]) * ease) + this._saved[key];
-			},this);
-			return this;
-		},
-		tooltipPosition : function(){
-			return {
-				x : this.x,
-				y : this.y
-			};
-		},
-		hasValue: function(){
-			return isNumber(this.value);
-		}
-	});
-
-	Chart.Element.extend = inherits;
-
-
-	Chart.Point = Chart.Element.extend({
-		display: true,
-		inRange: function(chartX,chartY){
-			var hitDetectionRange = this.hitDetectionRadius + this.radius;
-			return ((Math.pow(chartX-this.x, 2)+Math.pow(chartY-this.y, 2)) < Math.pow(hitDetectionRange,2));
-		},
-		draw : function(){
-			if (this.display){
-				var ctx = this.ctx;
-				ctx.beginPath();
-
-				ctx.arc(this.x, this.y, this.radius, 0, Math.PI*2);
-				ctx.closePath();
-
-				ctx.strokeStyle = this.strokeColor;
-				ctx.lineWidth = this.strokeWidth;
-
-				ctx.fillStyle = this.fillColor;
-
-				ctx.fill();
-				ctx.stroke();
-			}
-
-
-			//Quick debug for bezier curve splining
-			//Highlights control points and the line between them.
-			//Handy for dev - stripped in the min version.
-
-			// ctx.save();
-			// ctx.fillStyle = "black";
-			// ctx.strokeStyle = "black"
-			// ctx.beginPath();
-			// ctx.arc(this.controlPoints.inner.x,this.controlPoints.inner.y, 2, 0, Math.PI*2);
-			// ctx.fill();
-
-			// ctx.beginPath();
-			// ctx.arc(this.controlPoints.outer.x,this.controlPoints.outer.y, 2, 0, Math.PI*2);
-			// ctx.fill();
-
-			// ctx.moveTo(this.controlPoints.inner.x,this.controlPoints.inner.y);
-			// ctx.lineTo(this.x, this.y);
-			// ctx.lineTo(this.controlPoints.outer.x,this.controlPoints.outer.y);
-			// ctx.stroke();
-
-			// ctx.restore();
-
-
-
-		}
-	});
-
-	Chart.Arc = Chart.Element.extend({
-		inRange : function(chartX,chartY){
-
-			var pointRelativePosition = helpers.getAngleFromPoint(this, {
-				x: chartX,
-				y: chartY
-			});
-
-			//Check if within the range of the open/close angle
-			var betweenAngles = (pointRelativePosition.angle >= this.startAngle && pointRelativePosition.angle <= this.endAngle),
-				withinRadius = (pointRelativePosition.distance >= this.innerRadius && pointRelativePosition.distance <= this.outerRadius);
-
-			return (betweenAngles && withinRadius);
-			//Ensure within the outside of the arc centre, but inside arc outer
-		},
-		tooltipPosition : function(){
-			var centreAngle = this.startAngle + ((this.endAngle - this.startAngle) / 2),
-				rangeFromCentre = (this.outerRadius - this.innerRadius) / 2 + this.innerRadius;
-			return {
-				x : this.x + (Math.cos(centreAngle) * rangeFromCentre),
-				y : this.y + (Math.sin(centreAngle) * rangeFromCentre)
-			};
-		},
-		draw : function(animationPercent){
-
-			var easingDecimal = animationPercent || 1;
-
-			var ctx = this.ctx;
-
-			ctx.beginPath();
-
-			ctx.arc(this.x, this.y, this.outerRadius, this.startAngle, this.endAngle);
-
-			ctx.arc(this.x, this.y, this.innerRadius, this.endAngle, this.startAngle, true);
-
-			ctx.closePath();
-			ctx.strokeStyle = this.strokeColor;
-			ctx.lineWidth = this.strokeWidth;
-
-			ctx.fillStyle = this.fillColor;
-
-			ctx.fill();
-			ctx.lineJoin = 'bevel';
-
-			if (this.showStroke){
-				ctx.stroke();
-			}
-		}
-	});
-
-	Chart.Rectangle = Chart.Element.extend({
-		draw : function(){
-			var ctx = this.ctx,
-				halfWidth = this.width/2,
-				leftX = this.x - halfWidth,
-				rightX = this.x + halfWidth,
-				top = this.base - (this.base - this.y),
-				halfStroke = this.strokeWidth / 2;
-
-			// Canvas doesn't allow us to stroke inside the width so we can
-			// adjust the sizes to fit if we're setting a stroke on the line
-			if (this.showStroke){
-				leftX += halfStroke;
-				rightX -= halfStroke;
-				top += halfStroke;
-			}
-
-			ctx.beginPath();
-
-			ctx.fillStyle = this.fillColor;
-			ctx.strokeStyle = this.strokeColor;
-			ctx.lineWidth = this.strokeWidth;
-
-			// It'd be nice to keep this class totally generic to any rectangle
-			// and simply specify which border to miss out.
-			ctx.moveTo(leftX, this.base);
-			ctx.lineTo(leftX, top);
-			ctx.lineTo(rightX, top);
-			ctx.lineTo(rightX, this.base);
-			ctx.fill();
-			if (this.showStroke){
-				ctx.stroke();
-			}
-		},
-		height : function(){
-			return this.base - this.y;
-		},
-		inRange : function(chartX,chartY){
-			return (chartX >= this.x - this.width/2 && chartX <= this.x + this.width/2) && (chartY >= this.y && chartY <= this.base);
-		}
-	});
-
-	Chart.Tooltip = Chart.Element.extend({
-		draw : function(){
-
-			var ctx = this.chart.ctx;
-
-			ctx.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
-
-			this.xAlign = "center";
-			this.yAlign = "above";
-
-			//Distance between the actual element.y position and the start of the tooltip caret
-			var caretPadding = this.caretPadding = 2;
-
-			var tooltipWidth = ctx.measureText(this.text).width + 2*this.xPadding,
-				tooltipRectHeight = this.fontSize + 2*this.yPadding,
-				tooltipHeight = tooltipRectHeight + this.caretHeight + caretPadding;
-
-			if (this.x + tooltipWidth/2 >this.chart.width){
-				this.xAlign = "left";
-			} else if (this.x - tooltipWidth/2 < 0){
-				this.xAlign = "right";
-			}
-
-			if (this.y - tooltipHeight < 0){
-				this.yAlign = "below";
-			}
-
-
-			var tooltipX = this.x - tooltipWidth/2,
-				tooltipY = this.y - tooltipHeight;
-
-			ctx.fillStyle = this.fillColor;
-
-			// Custom Tooltips
-			if(this.custom){
-				this.custom(this);
-			}
-			else{
-				switch(this.yAlign)
-				{
-				case "above":
-					//Draw a caret above the x/y
-					ctx.beginPath();
-					ctx.moveTo(this.x,this.y - caretPadding);
-					ctx.lineTo(this.x + this.caretHeight, this.y - (caretPadding + this.caretHeight));
-					ctx.lineTo(this.x - this.caretHeight, this.y - (caretPadding + this.caretHeight));
-					ctx.closePath();
-					ctx.fill();
-					break;
-				case "below":
-					tooltipY = this.y + caretPadding + this.caretHeight;
-					//Draw a caret below the x/y
-					ctx.beginPath();
-					ctx.moveTo(this.x, this.y + caretPadding);
-					ctx.lineTo(this.x + this.caretHeight, this.y + caretPadding + this.caretHeight);
-					ctx.lineTo(this.x - this.caretHeight, this.y + caretPadding + this.caretHeight);
-					ctx.closePath();
-					ctx.fill();
-					break;
-				}
-
-				switch(this.xAlign)
-				{
-				case "left":
-					tooltipX = this.x - tooltipWidth + (this.cornerRadius + this.caretHeight);
-					break;
-				case "right":
-					tooltipX = this.x - (this.cornerRadius + this.caretHeight);
-					break;
-				}
-
-				drawRoundedRectangle(ctx,tooltipX,tooltipY,tooltipWidth,tooltipRectHeight,this.cornerRadius);
-
-				ctx.fill();
-
-				ctx.fillStyle = this.textColor;
-				ctx.textAlign = "center";
-				ctx.textBaseline = "middle";
-				ctx.fillText(this.text, tooltipX + tooltipWidth/2, tooltipY + tooltipRectHeight/2);
-			}
-		}
-	});
-
-	Chart.MultiTooltip = Chart.Element.extend({
-		initialize : function(){
-			this.font = fontString(this.fontSize,this.fontStyle,this.fontFamily);
-
-			this.titleFont = fontString(this.titleFontSize,this.titleFontStyle,this.titleFontFamily);
-
-			this.height = (this.labels.length * this.fontSize) + ((this.labels.length-1) * (this.fontSize/2)) + (this.yPadding*2) + this.titleFontSize *1.5;
-
-			this.ctx.font = this.titleFont;
-
-			var titleWidth = this.ctx.measureText(this.title).width,
-				//Label has a legend square as well so account for this.
-				labelWidth = longestText(this.ctx,this.font,this.labels) + this.fontSize + 3,
-				longestTextWidth = max([labelWidth,titleWidth]);
-
-			this.width = longestTextWidth + (this.xPadding*2);
-
-
-			var halfHeight = this.height/2;
-
-			//Check to ensure the height will fit on the canvas
-			if (this.y - halfHeight < 0 ){
-				this.y = halfHeight;
-			} else if (this.y + halfHeight > this.chart.height){
-				this.y = this.chart.height - halfHeight;
-			}
-
-			//Decide whether to align left or right based on position on canvas
-			if (this.x > this.chart.width/2){
-				this.x -= this.xOffset + this.width;
-			} else {
-				this.x += this.xOffset;
-			}
-
-
-		},
-		getLineHeight : function(index){
-			var baseLineHeight = this.y - (this.height/2) + this.yPadding,
-				afterTitleIndex = index-1;
-
-			//If the index is zero, we're getting the title
-			if (index === 0){
-				return baseLineHeight + this.titleFontSize/2;
-			} else{
-				return baseLineHeight + ((this.fontSize*1.5*afterTitleIndex) + this.fontSize/2) + this.titleFontSize * 1.5;
-			}
-
-		},
-		draw : function(){
-			// Custom Tooltips
-			if(this.custom){
-				this.custom(this);
-			}
-			else{
-				drawRoundedRectangle(this.ctx,this.x,this.y - this.height/2,this.width,this.height,this.cornerRadius);
-				var ctx = this.ctx;
-				ctx.fillStyle = this.fillColor;
-				ctx.fill();
-				ctx.closePath();
-
-				ctx.textAlign = "left";
-				ctx.textBaseline = "middle";
-				ctx.fillStyle = this.titleTextColor;
-				ctx.font = this.titleFont;
-
-				ctx.fillText(this.title,this.x + this.xPadding, this.getLineHeight(0));
-
-				ctx.font = this.font;
-				helpers.each(this.labels,function(label,index){
-					ctx.fillStyle = this.textColor;
-					ctx.fillText(label,this.x + this.xPadding + this.fontSize + 3, this.getLineHeight(index + 1));
-
-					//A bit gnarly, but clearing this rectangle breaks when using explorercanvas (clears whole canvas)
-					//ctx.clearRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
-					//Instead we'll make a white filled block to put the legendColour palette over.
-
-					ctx.fillStyle = this.legendColorBackground;
-					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
-
-					ctx.fillStyle = this.legendColors[index].fill;
-					ctx.fillRect(this.x + this.xPadding, this.getLineHeight(index + 1) - this.fontSize/2, this.fontSize, this.fontSize);
-
-
-				},this);
-			}
-		}
-	});
-
-	Chart.Scale = Chart.Element.extend({
-		initialize : function(){
-			this.fit();
-		},
-		buildYLabels : function(){
-			this.yLabels = [];
-
-			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
-
-			for (var i=0; i<=this.steps; i++){
-				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
-			}
-			this.yLabelWidth = (this.display && this.showLabels) ? longestText(this.ctx,this.font,this.yLabels) : 0;
-		},
-		addXLabel : function(label){
-			this.xLabels.push(label);
-			this.valuesCount++;
-			this.fit();
-		},
-		removeXLabel : function(){
-			this.xLabels.shift();
-			this.valuesCount--;
-			this.fit();
-		},
-		// Fitting loop to rotate x Labels and figure out what fits there, and also calculate how many Y steps to use
-		fit: function(){
-			// First we need the width of the yLabels, assuming the xLabels aren't rotated
-
-			// To do that we need the base line at the top and base of the chart, assuming there is no x label rotation
-			this.startPoint = (this.display) ? this.fontSize : 0;
-			this.endPoint = (this.display) ? this.height - (this.fontSize * 1.5) - 5 : this.height; // -5 to pad labels
-
-			// Apply padding settings to the start and end point.
-			this.startPoint += this.padding;
-			this.endPoint -= this.padding;
-
-			// Cache the starting height, so can determine if we need to recalculate the scale yAxis
-			var cachedHeight = this.endPoint - this.startPoint,
-				cachedYLabelWidth;
-
-			// Build the current yLabels so we have an idea of what size they'll be to start
-			/*
-			 *	This sets what is returned from calculateScaleRange as static properties of this class:
-			 *
-				this.steps;
-				this.stepValue;
-				this.min;
-				this.max;
-			 *
-			 */
-			this.calculateYRange(cachedHeight);
-
-			// With these properties set we can now build the array of yLabels
-			// and also the width of the largest yLabel
-			this.buildYLabels();
-
-			this.calculateXLabelRotation();
-
-			while((cachedHeight > this.endPoint - this.startPoint)){
-				cachedHeight = this.endPoint - this.startPoint;
-				cachedYLabelWidth = this.yLabelWidth;
-
-				this.calculateYRange(cachedHeight);
-				this.buildYLabels();
-
-				// Only go through the xLabel loop again if the yLabel width has changed
-				if (cachedYLabelWidth < this.yLabelWidth){
-					this.calculateXLabelRotation();
-				}
-			}
-
-		},
-		calculateXLabelRotation : function(){
-			//Get the width of each grid by calculating the difference
-			//between x offsets between 0 and 1.
-
-			this.ctx.font = this.font;
-
-			var firstWidth = this.ctx.measureText(this.xLabels[0]).width,
-				lastWidth = this.ctx.measureText(this.xLabels[this.xLabels.length - 1]).width,
-				firstRotated,
-				lastRotated;
-
-
-			this.xScalePaddingRight = lastWidth/2 + 3;
-			this.xScalePaddingLeft = (firstWidth/2 > this.yLabelWidth + 10) ? firstWidth/2 : this.yLabelWidth + 10;
-
-			this.xLabelRotation = 0;
-			if (this.display){
-				var originalLabelWidth = longestText(this.ctx,this.font,this.xLabels),
-					cosRotation,
-					firstRotatedWidth;
-				this.xLabelWidth = originalLabelWidth;
-				//Allow 3 pixels x2 padding either side for label readability
-				var xGridWidth = Math.floor(this.calculateX(1) - this.calculateX(0)) - 6;
-
-				//Max label rotate should be 90 - also act as a loop counter
-				while ((this.xLabelWidth > xGridWidth && this.xLabelRotation === 0) || (this.xLabelWidth > xGridWidth && this.xLabelRotation <= 90 && this.xLabelRotation > 0)){
-					cosRotation = Math.cos(toRadians(this.xLabelRotation));
-
-					firstRotated = cosRotation * firstWidth;
-					lastRotated = cosRotation * lastWidth;
-
-					// We're right aligning the text now.
-					if (firstRotated + this.fontSize / 2 > this.yLabelWidth + 8){
-						this.xScalePaddingLeft = firstRotated + this.fontSize / 2;
-					}
-					this.xScalePaddingRight = this.fontSize/2;
-
-
-					this.xLabelRotation++;
-					this.xLabelWidth = cosRotation * originalLabelWidth;
-
-				}
-				if (this.xLabelRotation > 0){
-					this.endPoint -= Math.sin(toRadians(this.xLabelRotation))*originalLabelWidth + 3;
-				}
-			}
-			else{
-				this.xLabelWidth = 0;
-				this.xScalePaddingRight = this.padding;
-				this.xScalePaddingLeft = this.padding;
-			}
-
-		},
-		// Needs to be overidden in each Chart type
-		// Otherwise we need to pass all the data into the scale class
-		calculateYRange: noop,
-		drawingArea: function(){
-			return this.startPoint - this.endPoint;
-		},
-		calculateY : function(value){
-			var scalingFactor = this.drawingArea() / (this.min - this.max);
-			return this.endPoint - (scalingFactor * (value - this.min));
-		},
-		calculateX : function(index){
-			var isRotated = (this.xLabelRotation > 0),
-				// innerWidth = (this.offsetGridLines) ? this.width - offsetLeft - this.padding : this.width - (offsetLeft + halfLabelWidth * 2) - this.padding,
-				innerWidth = this.width - (this.xScalePaddingLeft + this.xScalePaddingRight),
-				valueWidth = innerWidth/Math.max((this.valuesCount - ((this.offsetGridLines) ? 0 : 1)), 1),
-				valueOffset = (valueWidth * index) + this.xScalePaddingLeft;
-
-			if (this.offsetGridLines){
-				valueOffset += (valueWidth/2);
-			}
-
-			return Math.round(valueOffset);
-		},
-		update : function(newProps){
-			helpers.extend(this, newProps);
-			this.fit();
-		},
-		draw : function(){
-			var ctx = this.ctx,
-				yLabelGap = (this.endPoint - this.startPoint) / this.steps,
-				xStart = Math.round(this.xScalePaddingLeft);
-			if (this.display){
-				ctx.fillStyle = this.textColor;
-				ctx.font = this.font;
-				each(this.yLabels,function(labelString,index){
-					var yLabelCenter = this.endPoint - (yLabelGap * index),
-						linePositionY = Math.round(yLabelCenter),
-						drawHorizontalLine = this.showHorizontalLines;
-
-					ctx.textAlign = "right";
-					ctx.textBaseline = "middle";
-					if (this.showLabels){
-						ctx.fillText(labelString,xStart - 10,yLabelCenter);
-					}
-
-					// This is X axis, so draw it
-					if (index === 0 && !drawHorizontalLine){
-						drawHorizontalLine = true;
-					}
-
-					if (drawHorizontalLine){
-						ctx.beginPath();
-					}
-
-					if (index > 0){
-						// This is a grid line in the centre, so drop that
-						ctx.lineWidth = this.gridLineWidth;
-						ctx.strokeStyle = this.gridLineColor;
-					} else {
-						// This is the first line on the scale
-						ctx.lineWidth = this.lineWidth;
-						ctx.strokeStyle = this.lineColor;
-					}
-
-					linePositionY += helpers.aliasPixel(ctx.lineWidth);
-
-					if(drawHorizontalLine){
-						ctx.moveTo(xStart, linePositionY);
-						ctx.lineTo(this.width, linePositionY);
-						ctx.stroke();
-						ctx.closePath();
-					}
-
-					ctx.lineWidth = this.lineWidth;
-					ctx.strokeStyle = this.lineColor;
-					ctx.beginPath();
-					ctx.moveTo(xStart - 5, linePositionY);
-					ctx.lineTo(xStart, linePositionY);
-					ctx.stroke();
-					ctx.closePath();
-
-				},this);
-
-				each(this.xLabels,function(label,index){
-					var xPos = this.calculateX(index) + aliasPixel(this.lineWidth),
-						// Check to see if line/bar here and decide where to place the line
-						linePos = this.calculateX(index - (this.offsetGridLines ? 0.5 : 0)) + aliasPixel(this.lineWidth),
-						isRotated = (this.xLabelRotation > 0),
-						drawVerticalLine = this.showVerticalLines;
-
-					// This is Y axis, so draw it
-					if (index === 0 && !drawVerticalLine){
-						drawVerticalLine = true;
-					}
-
-					if (drawVerticalLine){
-						ctx.beginPath();
-					}
-
-					if (index > 0){
-						// This is a grid line in the centre, so drop that
-						ctx.lineWidth = this.gridLineWidth;
-						ctx.strokeStyle = this.gridLineColor;
-					} else {
-						// This is the first line on the scale
-						ctx.lineWidth = this.lineWidth;
-						ctx.strokeStyle = this.lineColor;
-					}
-
-					if (drawVerticalLine){
-						ctx.moveTo(linePos,this.endPoint);
-						ctx.lineTo(linePos,this.startPoint - 3);
-						ctx.stroke();
-						ctx.closePath();
-					}
-
-
-					ctx.lineWidth = this.lineWidth;
-					ctx.strokeStyle = this.lineColor;
-
-
-					// Small lines at the bottom of the base grid line
-					ctx.beginPath();
-					ctx.moveTo(linePos,this.endPoint);
-					ctx.lineTo(linePos,this.endPoint + 5);
-					ctx.stroke();
-					ctx.closePath();
-
-					ctx.save();
-					ctx.translate(xPos,(isRotated) ? this.endPoint + 12 : this.endPoint + 8);
-					ctx.rotate(toRadians(this.xLabelRotation)*-1);
-					ctx.font = this.font;
-					ctx.textAlign = (isRotated) ? "right" : "center";
-					ctx.textBaseline = (isRotated) ? "middle" : "top";
-					ctx.fillText(label, 0, 0);
-					ctx.restore();
-				},this);
-
-			}
-		}
-
-	});
-
-	Chart.RadialScale = Chart.Element.extend({
-		initialize: function(){
-			this.size = min([this.height, this.width]);
-			this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
-		},
-		calculateCenterOffset: function(value){
-			// Take into account half font size + the yPadding of the top value
-			var scalingFactor = this.drawingArea / (this.max - this.min);
-
-			return (value - this.min) * scalingFactor;
-		},
-		update : function(){
-			if (!this.lineArc){
-				this.setScaleSize();
-			} else {
-				this.drawingArea = (this.display) ? (this.size/2) - (this.fontSize/2 + this.backdropPaddingY) : (this.size/2);
-			}
-			this.buildYLabels();
-		},
-		buildYLabels: function(){
-			this.yLabels = [];
-
-			var stepDecimalPlaces = getDecimalPlaces(this.stepValue);
-
-			for (var i=0; i<=this.steps; i++){
-				this.yLabels.push(template(this.templateString,{value:(this.min + (i * this.stepValue)).toFixed(stepDecimalPlaces)}));
-			}
-		},
-		getCircumference : function(){
-			return ((Math.PI*2) / this.valuesCount);
-		},
-		setScaleSize: function(){
-			/*
-			 * Right, this is really confusing and there is a lot of maths going on here
-			 * The gist of the problem is here: https://gist.github.com/nnnick/696cc9c55f4b0beb8fe9
-			 *
-			 * Reaction: https://dl.dropboxusercontent.com/u/34601363/toomuchscience.gif
-			 *
-			 * Solution:
-			 *
-			 * We assume the radius of the polygon is half the size of the canvas at first
-			 * at each index we check if the text overlaps.
-			 *
-			 * Where it does, we store that angle and that index.
-			 *
-			 * After finding the largest index and angle we calculate how much we need to remove
-			 * from the shape radius to move the point inwards by that x.
-			 *
-			 * We average the left and right distances to get the maximum shape radius that can fit in the box
-			 * along with labels.
-			 *
-			 * Once we have that, we can find the centre point for the chart, by taking the x text protrusion
-			 * on each side, removing that from the size, halving it and adding the left x protrusion width.
-			 *
-			 * This will mean we have a shape fitted to the canvas, as large as it can be with the labels
-			 * and position it in the most space efficient manner
-			 *
-			 * https://dl.dropboxusercontent.com/u/34601363/yeahscience.gif
-			 */
-
-
-			// Get maximum radius of the polygon. Either half the height (minus the text width) or half the width.
-			// Use this to calculate the offset + change. - Make sure L/R protrusion is at least 0 to stop issues with centre points
-			var largestPossibleRadius = min([(this.height/2 - this.pointLabelFontSize - 5), this.width/2]),
-				pointPosition,
-				i,
-				textWidth,
-				halfTextWidth,
-				furthestRight = this.width,
-				furthestRightIndex,
-				furthestRightAngle,
-				furthestLeft = 0,
-				furthestLeftIndex,
-				furthestLeftAngle,
-				xProtrusionLeft,
-				xProtrusionRight,
-				radiusReductionRight,
-				radiusReductionLeft,
-				maxWidthRadius;
-			this.ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
-			for (i=0;i furthestRight) {
-						furthestRight = pointPosition.x + halfTextWidth;
-						furthestRightIndex = i;
-					}
-					if (pointPosition.x - halfTextWidth < furthestLeft) {
-						furthestLeft = pointPosition.x - halfTextWidth;
-						furthestLeftIndex = i;
-					}
-				}
-				else if (i < this.valuesCount/2) {
-					// Less than half the values means we'll left align the text
-					if (pointPosition.x + textWidth > furthestRight) {
-						furthestRight = pointPosition.x + textWidth;
-						furthestRightIndex = i;
-					}
-				}
-				else if (i > this.valuesCount/2){
-					// More than half the values means we'll right align the text
-					if (pointPosition.x - textWidth < furthestLeft) {
-						furthestLeft = pointPosition.x - textWidth;
-						furthestLeftIndex = i;
-					}
-				}
-			}
-
-			xProtrusionLeft = furthestLeft;
-
-			xProtrusionRight = Math.ceil(furthestRight - this.width);
-
-			furthestRightAngle = this.getIndexAngle(furthestRightIndex);
-
-			furthestLeftAngle = this.getIndexAngle(furthestLeftIndex);
-
-			radiusReductionRight = xProtrusionRight / Math.sin(furthestRightAngle + Math.PI/2);
-
-			radiusReductionLeft = xProtrusionLeft / Math.sin(furthestLeftAngle + Math.PI/2);
-
-			// Ensure we actually need to reduce the size of the chart
-			radiusReductionRight = (isNumber(radiusReductionRight)) ? radiusReductionRight : 0;
-			radiusReductionLeft = (isNumber(radiusReductionLeft)) ? radiusReductionLeft : 0;
-
-			this.drawingArea = largestPossibleRadius - (radiusReductionLeft + radiusReductionRight)/2;
-
-			//this.drawingArea = min([maxWidthRadius, (this.height - (2 * (this.pointLabelFontSize + 5)))/2])
-			this.setCenterPoint(radiusReductionLeft, radiusReductionRight);
-
-		},
-		setCenterPoint: function(leftMovement, rightMovement){
-
-			var maxRight = this.width - rightMovement - this.drawingArea,
-				maxLeft = leftMovement + this.drawingArea;
-
-			this.xCenter = (maxLeft + maxRight)/2;
-			// Always vertically in the centre as the text height doesn't change
-			this.yCenter = (this.height/2);
-		},
-
-		getIndexAngle : function(index){
-			var angleMultiplier = (Math.PI * 2) / this.valuesCount;
-			// Start from the top instead of right, so remove a quarter of the circle
-
-			return index * angleMultiplier - (Math.PI/2);
-		},
-		getPointPosition : function(index, distanceFromCenter){
-			var thisAngle = this.getIndexAngle(index);
-			return {
-				x : (Math.cos(thisAngle) * distanceFromCenter) + this.xCenter,
-				y : (Math.sin(thisAngle) * distanceFromCenter) + this.yCenter
-			};
-		},
-		draw: function(){
-			if (this.display){
-				var ctx = this.ctx;
-				each(this.yLabels, function(label, index){
-					// Don't draw a centre value
-					if (index > 0){
-						var yCenterOffset = index * (this.drawingArea/this.steps),
-							yHeight = this.yCenter - yCenterOffset,
-							pointPosition;
-
-						// Draw circular lines around the scale
-						if (this.lineWidth > 0){
-							ctx.strokeStyle = this.lineColor;
-							ctx.lineWidth = this.lineWidth;
-
-							if(this.lineArc){
-								ctx.beginPath();
-								ctx.arc(this.xCenter, this.yCenter, yCenterOffset, 0, Math.PI*2);
-								ctx.closePath();
-								ctx.stroke();
-							} else{
-								ctx.beginPath();
-								for (var i=0;i= 0; i--) {
-						if (this.angleLineWidth > 0){
-							var outerPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max));
-							ctx.beginPath();
-							ctx.moveTo(this.xCenter, this.yCenter);
-							ctx.lineTo(outerPosition.x, outerPosition.y);
-							ctx.stroke();
-							ctx.closePath();
-						}
-						// Extra 3px out for some label spacing
-						var pointLabelPosition = this.getPointPosition(i, this.calculateCenterOffset(this.max) + 5);
-						ctx.font = fontString(this.pointLabelFontSize,this.pointLabelFontStyle,this.pointLabelFontFamily);
-						ctx.fillStyle = this.pointLabelFontColor;
-
-						var labelsCount = this.labels.length,
-							halfLabelsCount = this.labels.length/2,
-							quarterLabelsCount = halfLabelsCount/2,
-							upperHalf = (i < quarterLabelsCount || i > labelsCount - quarterLabelsCount),
-							exactQuarter = (i === quarterLabelsCount || i === labelsCount - quarterLabelsCount);
-						if (i === 0){
-							ctx.textAlign = 'center';
-						} else if(i === halfLabelsCount){
-							ctx.textAlign = 'center';
-						} else if (i < halfLabelsCount){
-							ctx.textAlign = 'left';
-						} else {
-							ctx.textAlign = 'right';
-						}
-
-						// Set the correct text baseline based on outer positioning
-						if (exactQuarter){
-							ctx.textBaseline = 'middle';
-						} else if (upperHalf){
-							ctx.textBaseline = 'bottom';
-						} else {
-							ctx.textBaseline = 'top';
-						}
-
-						ctx.fillText(this.labels[i], pointLabelPosition.x, pointLabelPosition.y);
-					}
-				}
-			}
-		}
-	});
-
-	// Attach global event to resize each chart instance when the browser resizes
-	helpers.addEvent(window, "resize", (function(){
-		// Basic debounce of resize function so it doesn't hurt performance when resizing browser.
-		var timeout;
-		return function(){
-			clearTimeout(timeout);
-			timeout = setTimeout(function(){
-				each(Chart.instances,function(instance){
-					// If the responsive flag is set in the chart instance config
-					// Cascade the resize event down to the chart.
-					if (instance.options.responsive){
-						instance.resize(instance.render, true);
-					}
-				});
-			}, 50);
-		};
-	})());
-
-
-	if (amd) {
-		define(function(){
-			return Chart;
-		});
-	} else if (typeof module === 'object' && module.exports) {
-		module.exports = Chart;
-	}
-
-	root.Chart = Chart;
-
-	Chart.noConflict = function(){
-		root.Chart = previous;
-		return Chart;
-	};
-
-}).call(window);
-
-(function(){
-	"use strict";
-
-	var root = this,
-		Chart = root.Chart,
-		helpers = Chart.helpers;
-
-
-	var defaultConfig = {
-		//Boolean - Whether the scale should start at zero, or an order of magnitude down from the lowest value
-		scaleBeginAtZero : true,
-
-		//Boolean - Whether grid lines are shown across the chart
-		scaleShowGridLines : true,
-
-		//String - Colour of the grid lines
-		scaleGridLineColor : "rgba(0,0,0,.05)",
-
-		//Number - Width of the grid lines
-		scaleGridLineWidth : 1,
-
-		//Boolean - Whether to show horizontal lines (except X axis)
-		scaleShowHorizontalLines: true,
-
-		//Boolean - Whether to show vertical lines (except Y axis)
-		scaleShowVerticalLines: true,
-
-		//Boolean - If there is a stroke on each bar
-		barShowStroke : true,
-
-		//Number - Pixel width of the bar stroke
-		barStrokeWidth : 2,
-
-		//Number - Spacing between each of the X value sets
-		barValueSpacing : 5,
-
-		//Number - Spacing between data sets within X values
-		barDatasetSpacing : 1,
-
-		//String - A legend template
-		legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Bar", - defaults : defaultConfig, - initialize: function(data){ - - //Expose options as a scope variable here so we can access it in the ScaleClass - var options = this.options; - - this.ScaleClass = Chart.Scale.extend({ - offsetGridLines : true, - calculateBarX : function(datasetCount, datasetIndex, barIndex){ - //Reusable method for calculating the xPosition of a given bar based on datasetIndex & width of the bar - var xWidth = this.calculateBaseWidth(), - xAbsolute = this.calculateX(barIndex) - (xWidth/2), - barWidth = this.calculateBarWidth(datasetCount); - - return xAbsolute + (barWidth * datasetIndex) + (datasetIndex * options.barDatasetSpacing) + barWidth/2; - }, - calculateBaseWidth : function(){ - return (this.calculateX(1) - this.calculateX(0)) - (2*options.barValueSpacing); - }, - calculateBarWidth : function(datasetCount){ - //The padding between datasets is to the right of each bar, providing that there are more than 1 dataset - var baseWidth = this.calculateBaseWidth() - ((datasetCount - 1) * options.barDatasetSpacing); - - return (baseWidth / datasetCount); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeBars = (evt.type !== 'mouseout') ? this.getBarsAtEvent(evt) : []; - - this.eachBars(function(bar){ - bar.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activeBars, function(activeBar){ - activeBar.fillColor = activeBar.highlightFill; - activeBar.strokeColor = activeBar.highlightStroke; - }); - this.showTooltip(activeBars); - }); - } - - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.BarClass = Chart.Rectangle.extend({ - strokeWidth : this.options.barStrokeWidth, - showStroke : this.options.barShowStroke, - ctx : this.chart.ctx - }); - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset,datasetIndex){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - bars : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.bars.push(new this.BarClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.strokeColor, - fillColor : dataset.fillColor, - highlightFill : dataset.highlightFill || dataset.fillColor, - highlightStroke : dataset.highlightStroke || dataset.strokeColor - })); - },this); - - },this); - - this.buildScale(data.labels); - - this.BarClass.prototype.base = this.scale.endPoint; - - this.eachBars(function(bar, index, datasetIndex){ - helpers.extend(bar, { - width : this.scale.calculateBarWidth(this.datasets.length), - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y: this.scale.endPoint - }); - bar.save(); - }, this); - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - - this.eachBars(function(bar){ - bar.save(); - }); - this.render(); - }, - eachBars : function(callback){ - helpers.each(this.datasets,function(dataset, datasetIndex){ - helpers.each(dataset.bars, callback, this, datasetIndex); - },this); - }, - getBarsAtEvent : function(e){ - var barsArray = [], - eventPosition = helpers.getRelativePosition(e), - datasetIterator = function(dataset){ - barsArray.push(dataset.bars[barIndex]); - }, - barIndex; - - for (var datasetIndex = 0; datasetIndex < this.datasets.length; datasetIndex++) { - for (barIndex = 0; barIndex < this.datasets[datasetIndex].bars.length; barIndex++) { - if (this.datasets[datasetIndex].bars[barIndex].inRange(eventPosition.x,eventPosition.y)){ - helpers.each(this.datasets, datasetIterator); - return barsArray; - } - } - } - - return barsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachBars(function(bar){ - values.push(bar.value); - }); - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange: function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding : (this.options.showScale) ? 0 : (this.options.barShowStroke) ? this.options.barStrokeWidth : 0, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - this.scale = new this.ScaleClass(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].bars.push(new this.BarClass({ - value : value, - label : label, - x: this.scale.calculateBarX(this.datasets.length, datasetIndex, this.scale.valuesCount+1), - y: this.scale.endPoint, - width : this.scale.calculateBarWidth(this.datasets.length), - base : this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].strokeColor, - fillColor : this.datasets[datasetIndex].fillColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.bars.shift(); - },this); - this.update(); - }, - reflow : function(){ - helpers.extend(this.BarClass.prototype,{ - y: this.scale.endPoint, - base : this.scale.endPoint - }); - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - this.scale.draw(easingDecimal); - - //Draw all the bars for each dataset - helpers.each(this.datasets,function(dataset,datasetIndex){ - helpers.each(dataset.bars,function(bar,index){ - if (bar.hasValue()){ - bar.base = this.scale.endPoint; - //Transition then draw - bar.transition({ - x : this.scale.calculateBarX(this.datasets.length, datasetIndex, index), - y : this.scale.calculateY(bar.value), - width : this.scale.calculateBarWidth(this.datasets.length) - }, easingDecimal).draw(); - } - },this); - - },this); - } - }); - - -}).call(window); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Whether we should show a stroke on each segment - segmentShowStroke : true, - - //String - The colour of each segment stroke - segmentStrokeColor : "#fff", - - //Number - The width of each segment stroke - segmentStrokeWidth : 2, - - //The percentage of the chart that we cut out of the middle. - percentageInnerCutout : 50, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect - animationEasing : "easeOutBounce", - - //Boolean - Whether we animate the rotation of the Doughnut - animateRotate : true, - - //Boolean - Whether we animate scaling the Doughnut from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "Doughnut", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - - //Declare segments as a static property to prevent inheriting across the Chart type prototype - this.segments = []; - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - - this.SegmentArc = Chart.Arc.extend({ - ctx : this.chart.ctx, - x : this.chart.width/2, - y : this.chart.height/2 - }); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - this.calculateTotal(data); - - helpers.each(data,function(datapoint, index){ - this.addData(datapoint, index, true); - },this); - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - this.segments.splice(index, 0, new this.SegmentArc({ - value : segment.value, - outerRadius : (this.options.animateScale) ? 0 : this.outerRadius, - innerRadius : (this.options.animateScale) ? 0 : (this.outerRadius/100) * this.options.percentageInnerCutout, - fillColor : segment.color, - highlightColor : segment.highlight || segment.color, - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - startAngle : Math.PI * 1.5, - circumference : (this.options.animateRotate) ? 0 : this.calculateCircumference(segment.value), - label : segment.label - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - calculateCircumference : function(value){ - return (Math.PI*2)*(Math.abs(value) / this.total); - }, - calculateTotal : function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += Math.abs(segment.value); - },this); - }, - update : function(){ - this.calculateTotal(this.segments); - - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor']); - }); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - this.render(); - }, - - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.outerRadius = (helpers.min([this.chart.width,this.chart.height]) - this.options.segmentStrokeWidth/2)/2; - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - }); - }, this); - }, - draw : function(easeDecimal){ - var animDecimal = (easeDecimal) ? easeDecimal : 1; - this.clear(); - helpers.each(this.segments,function(segment,index){ - segment.transition({ - circumference : this.calculateCircumference(segment.value), - outerRadius : this.outerRadius, - innerRadius : (this.outerRadius/100) * this.options.percentageInnerCutout - },animDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - segment.draw(); - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length-1){ - this.segments[index+1].startAngle = segment.endAngle; - } - },this); - - } - }); - - Chart.types.Doughnut.extend({ - name : "Pie", - defaults : helpers.merge(defaultConfig,{percentageInnerCutout : 0}) - }); - -}).call(window); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - var defaultConfig = { - - ///Boolean - Whether grid lines are shown across the chart - scaleShowGridLines : true, - - //String - Colour of the grid lines - scaleGridLineColor : "rgba(0,0,0,.05)", - - //Number - Width of the grid lines - scaleGridLineWidth : 1, - - //Boolean - Whether to show horizontal lines (except X axis) - scaleShowHorizontalLines: true, - - //Boolean - Whether to show vertical lines (except Y axis) - scaleShowVerticalLines: true, - - //Boolean - Whether the line is curved between points - bezierCurve : true, - - //Number - Tension of the bezier curve between points - bezierCurveTension : 0.4, - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 4, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }; - - - Chart.Type.extend({ - name: "Line", - defaults : defaultConfig, - initialize: function(data){ - //Declare the extension of the default point, to cater for the options passed in to the constructor - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx, - inRange : function(mouseX){ - return (Math.pow(mouseX-this.x, 2) < Math.pow(this.radius + this.hitDetectionRadius,2)); - } - }); - - this.datasets = []; - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePoints = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePoints, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - this.showTooltip(activePoints); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label : dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - this.buildScale(data.labels); - - - this.eachPoints(function(point, index){ - helpers.extend(point, { - x: this.scale.calculateX(index), - y: this.scale.endPoint - }); - point.save(); - }, this); - - },this); - - - this.render(); - }, - update : function(){ - this.scale.update(); - // Reset any highlight colours before updating. - helpers.each(this.activeElements, function(activeElement){ - activeElement.restore(['fillColor', 'strokeColor']); - }); - this.eachPoints(function(point){ - point.save(); - }); - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - getPointsAtEvent : function(e){ - var pointsArray = [], - eventPosition = helpers.getRelativePosition(e); - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,function(point){ - if (point.inRange(eventPosition.x,eventPosition.y)) pointsArray.push(point); - }); - },this); - return pointsArray; - }, - buildScale : function(labels){ - var self = this; - - var dataTotal = function(){ - var values = []; - self.eachPoints(function(point){ - values.push(point.value); - }); - - return values; - }; - - var scaleOptions = { - templateString : this.options.scaleLabel, - height : this.chart.height, - width : this.chart.width, - ctx : this.chart.ctx, - textColor : this.options.scaleFontColor, - fontSize : this.options.scaleFontSize, - fontStyle : this.options.scaleFontStyle, - fontFamily : this.options.scaleFontFamily, - valuesCount : labels.length, - beginAtZero : this.options.scaleBeginAtZero, - integersOnly : this.options.scaleIntegersOnly, - calculateYRange : function(currentHeight){ - var updatedRanges = helpers.calculateScaleRange( - dataTotal(), - currentHeight, - this.fontSize, - this.beginAtZero, - this.integersOnly - ); - helpers.extend(this, updatedRanges); - }, - xLabels : labels, - font : helpers.fontString(this.options.scaleFontSize, this.options.scaleFontStyle, this.options.scaleFontFamily), - lineWidth : this.options.scaleLineWidth, - lineColor : this.options.scaleLineColor, - showHorizontalLines : this.options.scaleShowHorizontalLines, - showVerticalLines : this.options.scaleShowVerticalLines, - gridLineWidth : (this.options.scaleShowGridLines) ? this.options.scaleGridLineWidth : 0, - gridLineColor : (this.options.scaleShowGridLines) ? this.options.scaleGridLineColor : "rgba(0,0,0,0)", - padding: (this.options.showScale) ? 0 : this.options.pointDotRadius + this.options.pointDotStrokeWidth, - showLabels : this.options.scaleShowLabels, - display : this.options.showScale - }; - - if (this.options.scaleOverride){ - helpers.extend(scaleOptions, { - calculateYRange: helpers.noop, - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - }); - } - - - this.scale = new Chart.Scale(scaleOptions); - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - - helpers.each(valuesArray,function(value,datasetIndex){ - //Add a new point for each piece of data, passing any required data to draw. - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: this.scale.calculateX(this.scale.valuesCount+1), - y: this.scale.endPoint, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.addXLabel(label); - //Then re-render the chart. - this.update(); - }, - removeData : function(){ - this.scale.removeXLabel(); - //Then re-render the chart. - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.update(); - }, - reflow : function(){ - var newScaleProps = helpers.extend({ - height : this.chart.height, - width : this.chart.width - }); - this.scale.update(newScaleProps); - }, - draw : function(ease){ - var easingDecimal = ease || 1; - this.clear(); - - var ctx = this.chart.ctx; - - // Some helper methods for getting the next/prev points - var hasValue = function(item){ - return item.value !== null; - }, - nextPoint = function(point, collection, index){ - return helpers.findNextWhere(collection, hasValue, index) || point; - }, - previousPoint = function(point, collection, index){ - return helpers.findPreviousWhere(collection, hasValue, index) || point; - }; - - this.scale.draw(easingDecimal); - - - helpers.each(this.datasets,function(dataset){ - var pointsWithValues = helpers.where(dataset.points, hasValue); - - //Transition each point first so that the line and point drawing isn't out of sync - //We can use this extra loop to calculate the control points of this dataset also in this loop - - helpers.each(dataset.points, function(point, index){ - if (point.hasValue()){ - point.transition({ - y : this.scale.calculateY(point.value), - x : this.scale.calculateX(index) - }, easingDecimal); - } - },this); - - - // Control points need to be calculated in a seperate loop, because we need to know the current x/y of the point - // This would cause issues when there is no animation, because the y of the next point would be 0, so beziers would be skewed - if (this.options.bezierCurve){ - helpers.each(pointsWithValues, function(point, index){ - var tension = (index > 0 && index < pointsWithValues.length - 1) ? this.options.bezierCurveTension : 0; - point.controlPoints = helpers.splineCurve( - previousPoint(point, pointsWithValues, index), - point, - nextPoint(point, pointsWithValues, index), - tension - ); - - // Prevent the bezier going outside of the bounds of the graph - - // Cap puter bezier handles to the upper/lower scale bounds - if (point.controlPoints.outer.y > this.scale.endPoint){ - point.controlPoints.outer.y = this.scale.endPoint; - } - else if (point.controlPoints.outer.y < this.scale.startPoint){ - point.controlPoints.outer.y = this.scale.startPoint; - } - - // Cap inner bezier handles to the upper/lower scale bounds - if (point.controlPoints.inner.y > this.scale.endPoint){ - point.controlPoints.inner.y = this.scale.endPoint; - } - else if (point.controlPoints.inner.y < this.scale.startPoint){ - point.controlPoints.inner.y = this.scale.startPoint; - } - },this); - } - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - - helpers.each(pointsWithValues, function(point, index){ - if (index === 0){ - ctx.moveTo(point.x, point.y); - } - else{ - if(this.options.bezierCurve){ - var previous = previousPoint(point, pointsWithValues, index); - - ctx.bezierCurveTo( - previous.controlPoints.outer.x, - previous.controlPoints.outer.y, - point.controlPoints.inner.x, - point.controlPoints.inner.y, - point.x, - point.y - ); - } - else{ - ctx.lineTo(point.x,point.y); - } - } - }, this); - - ctx.stroke(); - - if (this.options.datasetFill && pointsWithValues.length > 0){ - //Round off the line by going to the base of the chart, back to the start, then fill. - ctx.lineTo(pointsWithValues[pointsWithValues.length - 1].x, this.scale.endPoint); - ctx.lineTo(pointsWithValues[0].x, this.scale.endPoint); - ctx.fillStyle = dataset.fillColor; - ctx.closePath(); - ctx.fill(); - } - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(pointsWithValues,function(point){ - point.draw(); - }); - },this); - } - }); - - -}).call(window); - -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - //Cache a local reference to Chart.helpers - helpers = Chart.helpers; - - var defaultConfig = { - //Boolean - Show a backdrop to the scale label - scaleShowLabelBackdrop : true, - - //String - The colour of the label backdrop - scaleBackdropColor : "rgba(255,255,255,0.75)", - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //Number - The backdrop padding above & below the label in pixels - scaleBackdropPaddingY : 2, - - //Number - The backdrop padding to the side of the label in pixels - scaleBackdropPaddingX : 2, - - //Boolean - Show line for each value in the scale - scaleShowLine : true, - - //Boolean - Stroke a line around each segment in the chart - segmentShowStroke : true, - - //String - The colour of the stroke on each segement. - segmentStrokeColor : "#fff", - - //Number - The width of the stroke value in pixels - segmentStrokeWidth : 2, - - //Number - Amount of animation steps - animationSteps : 100, - - //String - Animation easing effect. - animationEasing : "easeOutBounce", - - //Boolean - Whether to animate the rotation of the chart - animateRotate : true, - - //Boolean - Whether to animate scaling the chart from the centre - animateScale : false, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(segments[i].label){%><%=segments[i].label%><%}%>
  • <%}%>
" - }; - - - Chart.Type.extend({ - //Passing in a name registers this chart in the Chart namespace - name: "PolarArea", - //Providing a defaults will also register the deafults in the chart namespace - defaults : defaultConfig, - //Initialize is fired when the chart is initialized - Data is passed in as a parameter - //Config is automatically merged by the core of Chart.js, and is available at this.options - initialize: function(data){ - this.segments = []; - //Declare segment class as a chart instance specific class, so it can share props for this instance - this.SegmentArc = Chart.Arc.extend({ - showStroke : this.options.segmentShowStroke, - strokeWidth : this.options.segmentStrokeWidth, - strokeColor : this.options.segmentStrokeColor, - ctx : this.chart.ctx, - innerRadius : 0, - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - lineArc: true, - width: this.chart.width, - height: this.chart.height, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - valuesCount: data.length - }); - - this.updateScaleRange(data); - - this.scale.update(); - - helpers.each(data,function(segment,index){ - this.addData(segment,index,true); - },this); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activeSegments = (evt.type !== 'mouseout') ? this.getSegmentsAtEvent(evt) : []; - helpers.each(this.segments,function(segment){ - segment.restore(["fillColor"]); - }); - helpers.each(activeSegments,function(activeSegment){ - activeSegment.fillColor = activeSegment.highlightColor; - }); - this.showTooltip(activeSegments); - }); - } - - this.render(); - }, - getSegmentsAtEvent : function(e){ - var segmentsArray = []; - - var location = helpers.getRelativePosition(e); - - helpers.each(this.segments,function(segment){ - if (segment.inRange(location.x,location.y)) segmentsArray.push(segment); - },this); - return segmentsArray; - }, - addData : function(segment, atIndex, silent){ - var index = atIndex || this.segments.length; - - this.segments.splice(index, 0, new this.SegmentArc({ - fillColor: segment.color, - highlightColor: segment.highlight || segment.color, - label: segment.label, - value: segment.value, - outerRadius: (this.options.animateScale) ? 0 : this.scale.calculateCenterOffset(segment.value), - circumference: (this.options.animateRotate) ? 0 : this.scale.getCircumference(), - startAngle: Math.PI * 1.5 - })); - if (!silent){ - this.reflow(); - this.update(); - } - }, - removeData: function(atIndex){ - var indexToDelete = (helpers.isNumber(atIndex)) ? atIndex : this.segments.length-1; - this.segments.splice(indexToDelete, 1); - this.reflow(); - this.update(); - }, - calculateTotal: function(data){ - this.total = 0; - helpers.each(data,function(segment){ - this.total += segment.value; - },this); - this.scale.valuesCount = this.segments.length; - }, - updateScaleRange: function(datapoints){ - var valuesArray = []; - helpers.each(datapoints,function(segment){ - valuesArray.push(segment.value); - }); - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes, - { - size: helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - } - ); - - }, - update : function(){ - this.calculateTotal(this.segments); - - helpers.each(this.segments,function(segment){ - segment.save(); - }); - - this.reflow(); - this.render(); - }, - reflow : function(){ - helpers.extend(this.SegmentArc.prototype,{ - x : this.chart.width/2, - y : this.chart.height/2 - }); - this.updateScaleRange(this.segments); - this.scale.update(); - - helpers.extend(this.scale,{ - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - - helpers.each(this.segments, function(segment){ - segment.update({ - outerRadius : this.scale.calculateCenterOffset(segment.value) - }); - }, this); - - }, - draw : function(ease){ - var easingDecimal = ease || 1; - //Clear & draw the canvas - this.clear(); - helpers.each(this.segments,function(segment, index){ - segment.transition({ - circumference : this.scale.getCircumference(), - outerRadius : this.scale.calculateCenterOffset(segment.value) - },easingDecimal); - - segment.endAngle = segment.startAngle + segment.circumference; - - // If we've removed the first segment we need to set the first one to - // start at the top. - if (index === 0){ - segment.startAngle = Math.PI * 1.5; - } - - //Check to see if it's the last segment, if not get the next and update the start angle - if (index < this.segments.length - 1){ - this.segments[index+1].startAngle = segment.endAngle; - } - segment.draw(); - }, this); - this.scale.draw(); - } - }); - -}).call(window); -(function(){ - "use strict"; - - var root = this, - Chart = root.Chart, - helpers = Chart.helpers; - - - - Chart.Type.extend({ - name: "Radar", - defaults:{ - //Boolean - Whether to show lines for each scale point - scaleShowLine : true, - - //Boolean - Whether we show the angle lines out of the radar - angleShowLineOut : true, - - //Boolean - Whether to show labels on the scale - scaleShowLabels : false, - - // Boolean - Whether the scale should begin at zero - scaleBeginAtZero : true, - - //String - Colour of the angle line - angleLineColor : "rgba(0,0,0,.1)", - - //Number - Pixel width of the angle line - angleLineWidth : 1, - - //String - Point label font declaration - pointLabelFontFamily : "'Arial'", - - //String - Point label font weight - pointLabelFontStyle : "normal", - - //Number - Point label font size in pixels - pointLabelFontSize : 10, - - //String - Point label font colour - pointLabelFontColor : "#666", - - //Boolean - Whether to show a dot for each point - pointDot : true, - - //Number - Radius of each point dot in pixels - pointDotRadius : 3, - - //Number - Pixel width of point dot stroke - pointDotStrokeWidth : 1, - - //Number - amount extra to add to the radius to cater for hit detection outside the drawn point - pointHitDetectionRadius : 20, - - //Boolean - Whether to show a stroke for datasets - datasetStroke : true, - - //Number - Pixel width of dataset stroke - datasetStrokeWidth : 2, - - //Boolean - Whether to fill the dataset with a colour - datasetFill : true, - - //String - A legend template - legendTemplate : "
    -legend\"><% for (var i=0; i
  • \"><%if(datasets[i].label){%><%=datasets[i].label%><%}%>
  • <%}%>
" - - }, - - initialize: function(data){ - this.PointClass = Chart.Point.extend({ - strokeWidth : this.options.pointDotStrokeWidth, - radius : this.options.pointDotRadius, - display: this.options.pointDot, - hitDetectionRadius : this.options.pointHitDetectionRadius, - ctx : this.chart.ctx - }); - - this.datasets = []; - - this.buildScale(data); - - //Set up tooltip events on the chart - if (this.options.showTooltips){ - helpers.bindEvents(this, this.options.tooltipEvents, function(evt){ - var activePointsCollection = (evt.type !== 'mouseout') ? this.getPointsAtEvent(evt) : []; - - this.eachPoints(function(point){ - point.restore(['fillColor', 'strokeColor']); - }); - helpers.each(activePointsCollection, function(activePoint){ - activePoint.fillColor = activePoint.highlightFill; - activePoint.strokeColor = activePoint.highlightStroke; - }); - - this.showTooltip(activePointsCollection); - }); - } - - //Iterate through each of the datasets, and build this into a property of the chart - helpers.each(data.datasets,function(dataset){ - - var datasetObject = { - label: dataset.label || null, - fillColor : dataset.fillColor, - strokeColor : dataset.strokeColor, - pointColor : dataset.pointColor, - pointStrokeColor : dataset.pointStrokeColor, - points : [] - }; - - this.datasets.push(datasetObject); - - helpers.each(dataset.data,function(dataPoint,index){ - //Add a new point for each piece of data, passing any required data to draw. - var pointPosition; - if (!this.scale.animation){ - pointPosition = this.scale.getPointPosition(index, this.scale.calculateCenterOffset(dataPoint)); - } - datasetObject.points.push(new this.PointClass({ - value : dataPoint, - label : data.labels[index], - datasetLabel: dataset.label, - x: (this.options.animation) ? this.scale.xCenter : pointPosition.x, - y: (this.options.animation) ? this.scale.yCenter : pointPosition.y, - strokeColor : dataset.pointStrokeColor, - fillColor : dataset.pointColor, - highlightFill : dataset.pointHighlightFill || dataset.pointColor, - highlightStroke : dataset.pointHighlightStroke || dataset.pointStrokeColor - })); - },this); - - },this); - - this.render(); - }, - eachPoints : function(callback){ - helpers.each(this.datasets,function(dataset){ - helpers.each(dataset.points,callback,this); - },this); - }, - - getPointsAtEvent : function(evt){ - var mousePosition = helpers.getRelativePosition(evt), - fromCenter = helpers.getAngleFromPoint({ - x: this.scale.xCenter, - y: this.scale.yCenter - }, mousePosition); - - var anglePerIndex = (Math.PI * 2) /this.scale.valuesCount, - pointIndex = Math.round((fromCenter.angle - Math.PI * 1.5) / anglePerIndex), - activePointsCollection = []; - - // If we're at the top, make the pointIndex 0 to get the first of the array. - if (pointIndex >= this.scale.valuesCount || pointIndex < 0){ - pointIndex = 0; - } - - if (fromCenter.distance <= this.scale.drawingArea){ - helpers.each(this.datasets, function(dataset){ - activePointsCollection.push(dataset.points[pointIndex]); - }); - } - - return activePointsCollection; - }, - - buildScale : function(data){ - this.scale = new Chart.RadialScale({ - display: this.options.showScale, - fontStyle: this.options.scaleFontStyle, - fontSize: this.options.scaleFontSize, - fontFamily: this.options.scaleFontFamily, - fontColor: this.options.scaleFontColor, - showLabels: this.options.scaleShowLabels, - showLabelBackdrop: this.options.scaleShowLabelBackdrop, - backdropColor: this.options.scaleBackdropColor, - backdropPaddingY : this.options.scaleBackdropPaddingY, - backdropPaddingX: this.options.scaleBackdropPaddingX, - lineWidth: (this.options.scaleShowLine) ? this.options.scaleLineWidth : 0, - lineColor: this.options.scaleLineColor, - angleLineColor : this.options.angleLineColor, - angleLineWidth : (this.options.angleShowLineOut) ? this.options.angleLineWidth : 0, - // Point labels at the edge of each line - pointLabelFontColor : this.options.pointLabelFontColor, - pointLabelFontSize : this.options.pointLabelFontSize, - pointLabelFontFamily : this.options.pointLabelFontFamily, - pointLabelFontStyle : this.options.pointLabelFontStyle, - height : this.chart.height, - width: this.chart.width, - xCenter: this.chart.width/2, - yCenter: this.chart.height/2, - ctx : this.chart.ctx, - templateString: this.options.scaleLabel, - labels: data.labels, - valuesCount: data.datasets[0].data.length - }); - - this.scale.setScaleSize(); - this.updateScaleRange(data.datasets); - this.scale.buildYLabels(); - }, - updateScaleRange: function(datasets){ - var valuesArray = (function(){ - var totalDataArray = []; - helpers.each(datasets,function(dataset){ - if (dataset.data){ - totalDataArray = totalDataArray.concat(dataset.data); - } - else { - helpers.each(dataset.points, function(point){ - totalDataArray.push(point.value); - }); - } - }); - return totalDataArray; - })(); - - - var scaleSizes = (this.options.scaleOverride) ? - { - steps: this.options.scaleSteps, - stepValue: this.options.scaleStepWidth, - min: this.options.scaleStartValue, - max: this.options.scaleStartValue + (this.options.scaleSteps * this.options.scaleStepWidth) - } : - helpers.calculateScaleRange( - valuesArray, - helpers.min([this.chart.width, this.chart.height])/2, - this.options.scaleFontSize, - this.options.scaleBeginAtZero, - this.options.scaleIntegersOnly - ); - - helpers.extend( - this.scale, - scaleSizes - ); - - }, - addData : function(valuesArray,label){ - //Map the values array for each of the datasets - this.scale.valuesCount++; - helpers.each(valuesArray,function(value,datasetIndex){ - var pointPosition = this.scale.getPointPosition(this.scale.valuesCount, this.scale.calculateCenterOffset(value)); - this.datasets[datasetIndex].points.push(new this.PointClass({ - value : value, - label : label, - x: pointPosition.x, - y: pointPosition.y, - strokeColor : this.datasets[datasetIndex].pointStrokeColor, - fillColor : this.datasets[datasetIndex].pointColor - })); - },this); - - this.scale.labels.push(label); - - this.reflow(); - - this.update(); - }, - removeData : function(){ - this.scale.valuesCount--; - this.scale.labels.shift(); - helpers.each(this.datasets,function(dataset){ - dataset.points.shift(); - },this); - this.reflow(); - this.update(); - }, - update : function(){ - this.eachPoints(function(point){ - point.save(); - }); - this.reflow(); - this.render(); - }, - reflow: function(){ - helpers.extend(this.scale, { - width : this.chart.width, - height: this.chart.height, - size : helpers.min([this.chart.width, this.chart.height]), - xCenter: this.chart.width/2, - yCenter: this.chart.height/2 - }); - this.updateScaleRange(this.datasets); - this.scale.setScaleSize(); - this.scale.buildYLabels(); - }, - draw : function(ease){ - var easeDecimal = ease || 1, - ctx = this.chart.ctx; - this.clear(); - this.scale.draw(); - - helpers.each(this.datasets,function(dataset){ - - //Transition each point first so that the line and point drawing isn't out of sync - helpers.each(dataset.points,function(point,index){ - if (point.hasValue()){ - point.transition(this.scale.getPointPosition(index, this.scale.calculateCenterOffset(point.value)), easeDecimal); - } - },this); - - - - //Draw the line between all the points - ctx.lineWidth = this.options.datasetStrokeWidth; - ctx.strokeStyle = dataset.strokeColor; - ctx.beginPath(); - helpers.each(dataset.points,function(point,index){ - if (index === 0){ - ctx.moveTo(point.x,point.y); - } - else{ - ctx.lineTo(point.x,point.y); - } - },this); - ctx.closePath(); - ctx.stroke(); - - ctx.fillStyle = dataset.fillColor; - ctx.fill(); - - //Now draw the points over the line - //A little inefficient double looping, but better than the line - //lagging behind the point positions - helpers.each(dataset.points,function(point){ - if (point.hasValue()){ - point.draw(); - } - }); - - },this); - - } - - }); - - -}).call(window); - diff --git a/client/js/common/vendor/bootstrap-tour-standalone.js b/client/js/common/vendor/bootstrap-tour-standalone.js deleted file mode 100644 index 9155204b..00000000 --- a/client/js/common/vendor/bootstrap-tour-standalone.js +++ /dev/null @@ -1,1288 +0,0 @@ -// License: LGPL-3.0-or-later -/* =========================================================== -# bootstrap-tour - v0.9.3 -# http://bootstraptour.com -# ============================================================== -# Copyright 2012-2013 Ulrich Sossou -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -*/ -/* ======================================================================== - * Bootstrap: transition.js v3.1.1 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - 'WebkitTransition' : 'webkitTransitionEnd', - 'MozTransition' : 'transitionend', - 'OTransition' : 'oTransitionEnd otransitionend', - 'transition' : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false, $el = this - $(this).one($.support.transition.end, function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - }) - -}(jQuery); - -/* ======================================================================== - * Bootstrap: tooltip.js v3.1.1 - * http://getbootstrap.com/javascript/#tooltip - * Inspired by the original jQuery.tipsy by Jason Frame - * ======================================================================== - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // TOOLTIP PUBLIC CLASS DEFINITION - // =============================== - - var Tooltip = function (element, options) { - this.type = - this.options = - this.enabled = - this.timeout = - this.hoverState = - this.$element = null - - this.init('tooltip', element, options) - } - - Tooltip.DEFAULTS = { - animation: true, - placement: 'top', - selector: false, - template: '
', - trigger: 'hover focus', - title: '', - delay: 0, - html: false, - container: false - } - - Tooltip.prototype.init = function (type, element, options) { - this.enabled = true - this.type = type - this.$element = $(element) - this.options = this.getOptions(options) - - var triggers = this.options.trigger.split(' ') - - for (var i = triggers.length; i--;) { - var trigger = triggers[i] - - if (trigger == 'click') { - this.$element.on('click.' + this.type, this.options.selector, $.proxy(this.toggle, this)) - } else if (trigger != 'manual') { - var eventIn = trigger == 'hover' ? 'mouseenter' : 'focusin' - var eventOut = trigger == 'hover' ? 'mouseleave' : 'focusout' - - this.$element.on(eventIn + '.' + this.type, this.options.selector, $.proxy(this.enter, this)) - this.$element.on(eventOut + '.' + this.type, this.options.selector, $.proxy(this.leave, this)) - } - } - - this.options.selector ? - (this._options = $.extend({}, this.options, { trigger: 'manual', selector: '' })) : - this.fixTitle() - } - - Tooltip.prototype.getDefaults = function () { - return Tooltip.DEFAULTS - } - - Tooltip.prototype.getOptions = function (options) { - options = $.extend({}, this.getDefaults(), this.$element.data(), options) - - if (options.delay && typeof options.delay == 'number') { - options.delay = { - show: options.delay, - hide: options.delay - } - } - - return options - } - - Tooltip.prototype.getDelegateOptions = function () { - var options = {} - var defaults = this.getDefaults() - - this._options && $.each(this._options, function (key, value) { - if (defaults[key] != value) options[key] = value - }) - - return options - } - - Tooltip.prototype.enter = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) - - clearTimeout(self.timeout) - - self.hoverState = 'in' - - if (!self.options.delay || !self.options.delay.show) return self.show() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'in') self.show() - }, self.options.delay.show) - } - - Tooltip.prototype.leave = function (obj) { - var self = obj instanceof this.constructor ? - obj : $(obj.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) - - clearTimeout(self.timeout) - - self.hoverState = 'out' - - if (!self.options.delay || !self.options.delay.hide) return self.hide() - - self.timeout = setTimeout(function () { - if (self.hoverState == 'out') self.hide() - }, self.options.delay.hide) - } - - Tooltip.prototype.show = function () { - var e = $.Event('show.bs.' + this.type) - - if (this.hasContent() && this.enabled) { - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - var that = this; - - var $tip = this.tip() - - this.setContent() - - if (this.options.animation) $tip.addClass('fade') - - var placement = typeof this.options.placement == 'function' ? - this.options.placement.call(this, $tip[0], this.$element[0]) : - this.options.placement - - var autoToken = /\s?auto?\s?/i - var autoPlace = autoToken.test(placement) - if (autoPlace) placement = placement.replace(autoToken, '') || 'top' - - $tip - .detach() - .css({ top: 0, left: 0, display: 'block' }) - .addClass(placement) - - this.options.container ? $tip.appendTo(this.options.container) : $tip.insertAfter(this.$element) - - var pos = this.getPosition() - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (autoPlace) { - var $parent = this.$element.parent() - - var orgPlacement = placement - var docScroll = document.documentElement.scrollTop || document.body.scrollTop - var parentWidth = this.options.container == 'body' ? window.innerWidth : $parent.outerWidth() - var parentHeight = this.options.container == 'body' ? window.innerHeight : $parent.outerHeight() - var parentLeft = this.options.container == 'body' ? 0 : $parent.offset().left - - placement = placement == 'bottom' && pos.top + pos.height + actualHeight - docScroll > parentHeight ? 'top' : - placement == 'top' && pos.top - docScroll - actualHeight < 0 ? 'bottom' : - placement == 'right' && pos.right + actualWidth > parentWidth ? 'left' : - placement == 'left' && pos.left - actualWidth < parentLeft ? 'right' : - placement - - $tip - .removeClass(orgPlacement) - .addClass(placement) - } - - var calculatedOffset = this.getCalculatedOffset(placement, pos, actualWidth, actualHeight) - - this.applyPlacement(calculatedOffset, placement) - this.hoverState = null - - var complete = function() { - that.$element.trigger('shown.bs.' + that.type) - } - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one($.support.transition.end, complete) - .emulateTransitionEnd(150) : - complete() - } - } - - Tooltip.prototype.applyPlacement = function (offset, placement) { - var replace - var $tip = this.tip() - var width = $tip[0].offsetWidth - var height = $tip[0].offsetHeight - - // manually read margins because getBoundingClientRect includes difference - var marginTop = parseInt($tip.css('margin-top'), 10) - var marginLeft = parseInt($tip.css('margin-left'), 10) - - // we must check for NaN for ie 8/9 - if (isNaN(marginTop)) marginTop = 0 - if (isNaN(marginLeft)) marginLeft = 0 - - offset.top = offset.top + marginTop - offset.left = offset.left + marginLeft - - // $.fn.offset doesn't round pixel values - // so we use setOffset directly with our own function B-0 - $.offset.setOffset($tip[0], $.extend({ - using: function (props) { - $tip.css({ - top: Math.round(props.top), - left: Math.round(props.left) - }) - } - }, offset), 0) - - $tip.addClass('in') - - // check to see if placing tip in new offset caused the tip to resize itself - var actualWidth = $tip[0].offsetWidth - var actualHeight = $tip[0].offsetHeight - - if (placement == 'top' && actualHeight != height) { - replace = true - offset.top = offset.top + height - actualHeight - } - - if (/bottom|top/.test(placement)) { - var delta = 0 - - if (offset.left < 0) { - delta = offset.left * -2 - offset.left = 0 - - $tip.offset(offset) - - actualWidth = $tip[0].offsetWidth - actualHeight = $tip[0].offsetHeight - } - - this.replaceArrow(delta - width + actualWidth, actualWidth, 'left') - } else { - this.replaceArrow(actualHeight - height, actualHeight, 'top') - } - - if (replace) $tip.offset(offset) - } - - Tooltip.prototype.replaceArrow = function (delta, dimension, position) { - this.arrow().css(position, delta ? (50 * (1 - delta / dimension) + '%') : '') - } - - Tooltip.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - - $tip.find('.tooltip-inner')[this.options.html ? 'html' : 'text'](title) - $tip.removeClass('fade in top bottom left right') - } - - Tooltip.prototype.hide = function () { - var that = this - var $tip = this.tip() - var e = $.Event('hide.bs.' + this.type) - - function complete() { - if (that.hoverState != 'in') $tip.detach() - that.$element.trigger('hidden.bs.' + that.type) - } - - this.$element.trigger(e) - - if (e.isDefaultPrevented()) return - - $tip.removeClass('in') - - $.support.transition && this.$tip.hasClass('fade') ? - $tip - .one($.support.transition.end, complete) - .emulateTransitionEnd(150) : - complete() - - this.hoverState = null - - return this - } - - Tooltip.prototype.fixTitle = function () { - var $e = this.$element - if ($e.attr('title') || typeof($e.attr('data-original-title')) != 'string') { - $e.attr('data-original-title', $e.attr('title') || '').attr('title', '') - } - } - - Tooltip.prototype.hasContent = function () { - return this.getTitle() - } - - Tooltip.prototype.getPosition = function () { - var el = this.$element[0] - return $.extend({}, (typeof el.getBoundingClientRect == 'function') ? el.getBoundingClientRect() : { - width: el.offsetWidth, - height: el.offsetHeight - }, this.$element.offset()) - } - - Tooltip.prototype.getCalculatedOffset = function (placement, pos, actualWidth, actualHeight) { - return placement == 'bottom' ? { top: pos.top + pos.height, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'top' ? { top: pos.top - actualHeight, left: pos.left + pos.width / 2 - actualWidth / 2 } : - placement == 'left' ? { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left - actualWidth } : - /* placement == 'right' */ { top: pos.top + pos.height / 2 - actualHeight / 2, left: pos.left + pos.width } - } - - Tooltip.prototype.getTitle = function () { - var title - var $e = this.$element - var o = this.options - - title = $e.attr('data-original-title') - || (typeof o.title == 'function' ? o.title.call($e[0]) : o.title) - - return title - } - - Tooltip.prototype.tip = function () { - return this.$tip = this.$tip || $(this.options.template) - } - - Tooltip.prototype.arrow = function () { - return this.$arrow = this.$arrow || this.tip().find('.tooltip-arrow') - } - - Tooltip.prototype.validate = function () { - if (!this.$element[0].parentNode) { - this.hide() - this.$element = null - this.options = null - } - } - - Tooltip.prototype.enable = function () { - this.enabled = true - } - - Tooltip.prototype.disable = function () { - this.enabled = false - } - - Tooltip.prototype.toggleEnabled = function () { - this.enabled = !this.enabled - } - - Tooltip.prototype.toggle = function (e) { - var self = e ? $(e.currentTarget)[this.type](this.getDelegateOptions()).data('bs.' + this.type) : this - self.tip().hasClass('in') ? self.leave(self) : self.enter(self) - } - - Tooltip.prototype.destroy = function () { - clearTimeout(this.timeout) - this.hide().$element.off('.' + this.type).removeData('bs.' + this.type) - } - - - // TOOLTIP PLUGIN DEFINITION - // ========================= - - var old = $.fn.tooltip - - $.fn.tooltip = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.tooltip') - var options = typeof option == 'object' && option - - if (!data && option == 'destroy') return - if (!data) $this.data('bs.tooltip', (data = new Tooltip(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.tooltip.Constructor = Tooltip - - - // TOOLTIP NO CONFLICT - // =================== - - $.fn.tooltip.noConflict = function () { - $.fn.tooltip = old - return this - } - -}(jQuery); - -/* ======================================================================== - * Bootstrap: popover.js v3.1.1 - * http://getbootstrap.com/javascript/#popovers - * ======================================================================== - * Copyright 2011-2014 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // POPOVER PUBLIC CLASS DEFINITION - // =============================== - - var Popover = function (element, options) { - this.init('popover', element, options) - } - - if (!$.fn.tooltip) throw new Error('Popover requires tooltip.js') - - Popover.DEFAULTS = $.extend({}, $.fn.tooltip.Constructor.DEFAULTS, { - placement: 'right', - trigger: 'click', - content: '', - template: '

' - }) - - - // NOTE: POPOVER EXTENDS tooltip.js - // ================================ - - Popover.prototype = $.extend({}, $.fn.tooltip.Constructor.prototype) - - Popover.prototype.constructor = Popover - - Popover.prototype.getDefaults = function () { - return Popover.DEFAULTS - } - - Popover.prototype.setContent = function () { - var $tip = this.tip() - var title = this.getTitle() - var content = this.getContent() - - $tip.find('.popover-title')[this.options.html ? 'html' : 'text'](title) - $tip.find('.popover-content')[ // we use append for html objects to maintain js events - this.options.html ? (typeof content == 'string' ? 'html' : 'append') : 'text' - ](content) - - $tip.removeClass('fade top bottom left right in') - - // IE8 doesn't accept hiding via the `:empty` pseudo selector, we have to do - // this manually by checking the contents. - if (!$tip.find('.popover-title').html()) $tip.find('.popover-title').hide() - } - - Popover.prototype.hasContent = function () { - return this.getTitle() || this.getContent() - } - - Popover.prototype.getContent = function () { - var $e = this.$element - var o = this.options - - return $e.attr('data-content') - || (typeof o.content == 'function' ? - o.content.call($e[0]) : - o.content) - } - - Popover.prototype.arrow = function () { - return this.$arrow = this.$arrow || this.tip().find('.arrow') - } - - Popover.prototype.tip = function () { - if (!this.$tip) this.$tip = $(this.options.template) - return this.$tip - } - - - // POPOVER PLUGIN DEFINITION - // ========================= - - var old = $.fn.popover - - $.fn.popover = function (option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.popover') - var options = typeof option == 'object' && option - - if (!data && option == 'destroy') return - if (!data) $this.data('bs.popover', (data = new Popover(this, options))) - if (typeof option == 'string') data[option]() - }) - } - - $.fn.popover.Constructor = Popover - - - // POPOVER NO CONFLICT - // =================== - - $.fn.popover.noConflict = function () { - $.fn.popover = old - return this - } - -}(jQuery); - -(function($, window) { - var Tour, document; - document = window.document; - Tour = (function() { - function Tour(options) { - var storage; - try { - storage = window.localStorage; - } catch (_error) { - storage = false; - } - this._options = $.extend({ - name: "tour", - steps: [], - container: "body", - keyboard: true, - storage: storage, - debug: false, - backdrop: false, - redirect: true, - orphan: false, - duration: false, - basePath: "", - template: "

", - afterSetState: function(key, value) {}, - afterGetState: function(key, value) {}, - afterRemoveState: function(key) {}, - onStart: function(tour) {}, - onEnd: function(tour) {}, - onShow: function(tour) {}, - onShown: function(tour) {}, - onHide: function(tour) {}, - onHidden: function(tour) {}, - onNext: function(tour) {}, - onPrev: function(tour) {}, - onPause: function(tour, duration) {}, - onResume: function(tour, duration) {} - }, options); - this._force = false; - this._inited = false; - this.backdrop = { - overlay: null, - $element: null, - $background: null, - backgroundShown: false, - overlayElementShown: false - }; - this; - } - - Tour.prototype.addSteps = function(steps) { - var step, _i, _len; - for (_i = 0, _len = steps.length; _i < _len; _i++) { - step = steps[_i]; - this.addStep(step); - } - return this; - }; - - Tour.prototype.addStep = function(step) { - this._options.steps.push(step); - return this; - }; - - Tour.prototype.getStep = function(i) { - if (this._options.steps[i] != null) { - return $.extend({ - id: "step-" + i, - path: "", - placement: "right", - title: "", - content: "

", - next: i === this._options.steps.length - 1 ? -1 : i + 1, - prev: i - 1, - animation: true, - container: this._options.container, - backdrop: this._options.backdrop, - redirect: this._options.redirect, - orphan: this._options.orphan, - duration: this._options.duration, - template: this._options.template, - onShow: this._options.onShow, - onShown: this._options.onShown, - onHide: this._options.onHide, - onHidden: this._options.onHidden, - onNext: this._options.onNext, - onPrev: this._options.onPrev, - onPause: this._options.onPause, - onResume: this._options.onResume - }, this._options.steps[i]); - } - }; - - Tour.prototype.init = function(force) { - this._force = force; - if (this.ended()) { - this._debug("Tour ended, init prevented."); - return this; - } - this.setCurrentStep(); - this._initMouseNavigation(); - this._initKeyboardNavigation(); - this._onResize((function(_this) { - return function() { - return _this.showStep(_this._current); - }; - })(this)); - if (this._current !== null) { - this.showStep(this._current); - } - this._inited = true; - return this; - }; - - Tour.prototype.start = function(force) { - var promise; - if (force == null) { - force = false; - } - if (!this._inited) { - this.init(force); - } - if (this._current === null) { - promise = this._makePromise(this._options.onStart != null ? this._options.onStart(this) : void 0); - this._callOnPromiseDone(promise, this.showStep, 0); - } - return this; - }; - - Tour.prototype.next = function() { - var promise; - promise = this.hideStep(this._current); - return this._callOnPromiseDone(promise, this._showNextStep); - }; - - Tour.prototype.prev = function() { - var promise; - promise = this.hideStep(this._current); - return this._callOnPromiseDone(promise, this._showPrevStep); - }; - - Tour.prototype.goTo = function(i) { - var promise; - promise = this.hideStep(this._current); - return this._callOnPromiseDone(promise, this.showStep, i); - }; - - Tour.prototype.end = function() { - var endHelper, promise; - endHelper = (function(_this) { - return function(e) { - $(document).off("click.tour-" + _this._options.name); - $(document).off("keyup.tour-" + _this._options.name); - $(window).off("resize.tour-" + _this._options.name); - _this._setState("end", "yes"); - _this._inited = false; - _this._force = false; - _this._clearTimer(); - if (_this._options.onEnd != null) { - return _this._options.onEnd(_this); - } - }; - })(this); - promise = this.hideStep(this._current); - return this._callOnPromiseDone(promise, endHelper); - }; - - Tour.prototype.ended = function() { - return !this._force && !!this._getState("end"); - }; - - Tour.prototype.restart = function() { - this._removeState("current_step"); - this._removeState("end"); - return this.start(); - }; - - Tour.prototype.pause = function() { - var step; - step = this.getStep(this._current); - if (!(step && step.duration)) { - return this; - } - this._paused = true; - this._duration -= new Date().getTime() - this._start; - window.clearTimeout(this._timer); - this._debug("Paused/Stopped step " + (this._current + 1) + " timer (" + this._duration + " remaining)."); - if (step.onPause != null) { - return step.onPause(this, this._duration); - } - }; - - Tour.prototype.resume = function() { - var step; - step = this.getStep(this._current); - if (!(step && step.duration)) { - return this; - } - this._paused = false; - this._start = new Date().getTime(); - this._duration = this._duration || step.duration; - this._timer = window.setTimeout((function(_this) { - return function() { - if (_this._isLast()) { - return _this.next(); - } else { - return _this.end(); - } - }; - })(this), this._duration); - this._debug("Started step " + (this._current + 1) + " timer with duration " + this._duration); - if ((step.onResume != null) && this._duration !== step.duration) { - return step.onResume(this, this._duration); - } - }; - - Tour.prototype.hideStep = function(i) { - var hideStepHelper, promise, step; - step = this.getStep(i); - if (!step) { - return; - } - this._clearTimer(); - promise = this._makePromise(step.onHide != null ? step.onHide(this, i) : void 0); - hideStepHelper = (function(_this) { - return function(e) { - var $element; - $element = $(step.element); - if (!($element.data("bs.popover") || $element.data("popover"))) { - $element = $("body"); - } - $element.popover("destroy").removeClass("tour-" + _this._options.name + "-element tour-" + _this._options.name + "-" + i + "-element"); - if (step.reflex) { - $element.css("cursor", "").off("click.tour-" + _this._options.name); - } - if (step.backdrop) { - _this._hideBackdrop(); - } - if (step.onHidden != null) { - return step.onHidden(_this); - } - }; - })(this); - this._callOnPromiseDone(promise, hideStepHelper); - return promise; - }; - - Tour.prototype.showStep = function(i) { - var promise, showStepHelper, skipToPrevious, step; - if (this.ended()) { - this._debug("Tour ended, showStep prevented."); - return this; - } - step = this.getStep(i); - if (!step) { - return; - } - skipToPrevious = i < this._current; - promise = this._makePromise(step.onShow != null ? step.onShow(this, i) : void 0); - showStepHelper = (function(_this) { - return function(e) { - var current_path, path; - _this.setCurrentStep(i); - path = (function() { - switch ({}.toString.call(step.path)) { - case "[object Function]": - return step.path(); - case "[object String]": - return this._options.basePath + step.path; - default: - return step.path; - } - }).call(_this); - current_path = [document.location.pathname, document.location.hash].join(""); - if (_this._isRedirect(path, current_path)) { - _this._redirect(step, path); - return; - } - if (_this._isOrphan(step)) { - if (!step.orphan) { - _this._debug("Skip the orphan step " + (_this._current + 1) + ". Orphan option is false and the element doesn't exist or is hidden."); - if (skipToPrevious) { - _this._showPrevStep(); - } else { - _this._showNextStep(); - } - return; - } - _this._debug("Show the orphan step " + (_this._current + 1) + ". Orphans option is true."); - } - if (step.backdrop) { - _this._showBackdrop(!_this._isOrphan(step) ? step.element : void 0); - } - _this._scrollIntoView(step.element, function() { - if (_this.getCurrentStep() !== i) { - return; - } - if ((step.element != null) && step.backdrop) { - _this._showOverlayElement(step.element); - } - _this._showPopover(step, i); - if (step.onShown != null) { - step.onShown(_this); - } - return _this._debug("Step " + (_this._current + 1) + " of " + _this._options.steps.length); - }); - if (step.duration) { - return _this.resume(); - } - }; - })(this); - this._callOnPromiseDone(promise, showStepHelper); - return promise; - }; - - Tour.prototype.getCurrentStep = function() { - return this._current; - }; - - Tour.prototype.setCurrentStep = function(value) { - if (value != null) { - this._current = value; - this._setState("current_step", value); - } else { - this._current = this._getState("current_step"); - this._current = this._current === null ? null : parseInt(this._current, 10); - } - return this; - }; - - Tour.prototype._setState = function(key, value) { - var e, keyName; - if (this._options.storage) { - keyName = "" + this._options.name + "_" + key; - try { - this._options.storage.setItem(keyName, value); - } catch (_error) { - e = _error; - if (e.code === DOMException.QUOTA_EXCEEDED_ERR) { - this.debug("LocalStorage quota exceeded. State storage failed."); - } - } - return this._options.afterSetState(keyName, value); - } else { - if (this._state == null) { - this._state = {}; - } - return this._state[key] = value; - } - }; - - Tour.prototype._removeState = function(key) { - var keyName; - if (this._options.storage) { - keyName = "" + this._options.name + "_" + key; - this._options.storage.removeItem(keyName); - return this._options.afterRemoveState(keyName); - } else { - if (this._state != null) { - return delete this._state[key]; - } - } - }; - - Tour.prototype._getState = function(key) { - var keyName, value; - if (this._options.storage) { - keyName = "" + this._options.name + "_" + key; - value = this._options.storage.getItem(keyName); - } else { - if (this._state != null) { - value = this._state[key]; - } - } - if (value === void 0 || value === "null") { - value = null; - } - this._options.afterGetState(key, value); - return value; - }; - - Tour.prototype._showNextStep = function() { - var promise, showNextStepHelper, step; - step = this.getStep(this._current); - showNextStepHelper = (function(_this) { - return function(e) { - return _this.showStep(step.next); - }; - })(this); - promise = this._makePromise(step.onNext != null ? step.onNext(this) : void 0); - return this._callOnPromiseDone(promise, showNextStepHelper); - }; - - Tour.prototype._showPrevStep = function() { - var promise, showPrevStepHelper, step; - step = this.getStep(this._current); - showPrevStepHelper = (function(_this) { - return function(e) { - return _this.showStep(step.prev); - }; - })(this); - promise = this._makePromise(step.onPrev != null ? step.onPrev(this) : void 0); - return this._callOnPromiseDone(promise, showPrevStepHelper); - }; - - Tour.prototype._debug = function(text) { - if (this._options.debug) { - return window.console.log("Bootstrap Tour '" + this._options.name + "' | " + text); - } - }; - - Tour.prototype._isRedirect = function(path, currentPath) { - return (path != null) && path !== "" && (({}.toString.call(path) === "[object RegExp]" && !path.test(currentPath)) || ({}.toString.call(path) === "[object String]" && path.replace(/\?.*$/, "").replace(/\/?$/, "") !== currentPath.replace(/\/?$/, ""))); - }; - - Tour.prototype._redirect = function(step, path) { - if ($.isFunction(step.redirect)) { - return step.redirect.call(this, path); - } else if (step.redirect === true) { - this._debug("Redirect to " + path); - return document.location.href = path; - } - }; - - Tour.prototype._isOrphan = function(step) { - return (step.element == null) || !$(step.element).length || $(step.element).is(":hidden") && ($(step.element)[0].namespaceURI !== "http://www.w3.org/2000/svg"); - }; - - Tour.prototype._isLast = function() { - return this._current < this._options.steps.length - 1; - }; - - Tour.prototype._showPopover = function(step, i) { - var $element, $navigation, $template, $tip, isOrphan, options; - $(".tour-" + this._options.name).remove(); - options = $.extend({}, this._options); - $template = $.isFunction(step.template) ? $(step.template(i, step)) : $(step.template); - $navigation = $template.find(".popover-navigation"); - isOrphan = this._isOrphan(step); - if (isOrphan) { - step.element = "body"; - step.placement = "top"; - $template = $template.addClass("orphan"); - } - $element = $(step.element); - $template.addClass("tour-" + this._options.name + " tour-" + this._options.name + "-" + i); - $element.addClass("tour-" + this._options.name + "-element tour-" + this._options.name + "-" + i + "-element"); - if (step.options) { - $.extend(options, step.options); - } - if (step.reflex && !isOrphan) { - $element.css("cursor", "pointer").on("click.tour-" + this._options.name, (function(_this) { - return function() { - if (_this._isLast()) { - return _this.next(); - } else { - return _this.end(); - } - }; - })(this)); - } - if (step.prev < 0) { - $navigation.find("[data-role='prev']").addClass("disabled"); - } - if (step.next < 0) { - $navigation.find("[data-role='next']").addClass("disabled"); - } - if (!step.duration) { - $navigation.find("[data-role='pause-resume']").remove(); - } - step.template = $template.clone().wrap("
").parent().html(); - $element.popover({ - placement: step.placement, - trigger: "manual", - title: step.title, - content: step.content, - html: true, - animation: step.animation, - container: step.container, - template: step.template, - selector: step.element - }).popover("show"); - $tip = $element.data("bs.popover") ? $element.data("bs.popover").tip() : $element.data("popover").tip(); - $tip.attr("id", step.id); - this._reposition($tip, step); - if (isOrphan) { - return this._center($tip); - } - }; - - Tour.prototype._reposition = function($tip, step) { - var offsetBottom, offsetHeight, offsetRight, offsetWidth, originalLeft, originalTop, tipOffset; - offsetWidth = $tip[0].offsetWidth; - offsetHeight = $tip[0].offsetHeight; - tipOffset = $tip.offset(); - originalLeft = tipOffset.left; - originalTop = tipOffset.top; - offsetBottom = $(document).outerHeight() - tipOffset.top - $tip.outerHeight(); - if (offsetBottom < 0) { - tipOffset.top = tipOffset.top + offsetBottom; - } - offsetRight = $("html").outerWidth() - tipOffset.left - $tip.outerWidth(); - if (offsetRight < 0) { - tipOffset.left = tipOffset.left + offsetRight; - } - if (tipOffset.top < 0) { - tipOffset.top = 0; - } - if (tipOffset.left < 0) { - tipOffset.left = 0; - } - $tip.offset(tipOffset); - if (step.placement === "bottom" || step.placement === "top") { - if (originalLeft !== tipOffset.left) { - return this._replaceArrow($tip, (tipOffset.left - originalLeft) * 2, offsetWidth, "left"); - } - } else { - if (originalTop !== tipOffset.top) { - return this._replaceArrow($tip, (tipOffset.top - originalTop) * 2, offsetHeight, "top"); - } - } - }; - - Tour.prototype._center = function($tip) { - return $tip.css("top", $(window).outerHeight() / 2 - $tip.outerHeight() / 2); - }; - - Tour.prototype._replaceArrow = function($tip, delta, dimension, position) { - return $tip.find(".arrow").css(position, delta ? 50 * (1 - delta / dimension) + "%" : ""); - }; - - Tour.prototype._scrollIntoView = function(element, callback) { - var $element, $window, counter, offsetTop, scrollTop, windowHeight; - $element = $(element); - if (!$element.length) { - return callback(); - } - $window = $(window); - offsetTop = $element.offset().top; - windowHeight = $window.height(); - scrollTop = Math.max(0, offsetTop - (windowHeight / 2)); - this._debug("Scroll into view. ScrollTop: " + scrollTop + ". Element offset: " + offsetTop + ". Window height: " + windowHeight + "."); - counter = 0; - return $("body,html").stop(true, true).animate({ - scrollTop: Math.ceil(scrollTop) - }, (function(_this) { - return function() { - if (++counter === 2) { - callback(); - return _this._debug("Scroll into view. Animation end element offset: " + ($element.offset().top) + ". Window height: " + ($window.height()) + "."); - } - }; - })(this)); - }; - - Tour.prototype._onResize = function(callback, timeout) { - return $(window).on("resize.tour-" + this._options.name, function() { - clearTimeout(timeout); - return timeout = setTimeout(callback, 100); - }); - }; - - Tour.prototype._initMouseNavigation = function() { - var _this; - _this = this; - return $(document).off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='prev']:not(.disabled)").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='next']:not(.disabled)").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='end']").off("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='pause-resume']").on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='next']:not(.disabled)", (function(_this) { - return function(e) { - e.preventDefault(); - return _this.next(); - }; - })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='prev']:not(.disabled)", (function(_this) { - return function(e) { - e.preventDefault(); - return _this.prev(); - }; - })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='end']", (function(_this) { - return function(e) { - e.preventDefault(); - return _this.end(); - }; - })(this)).on("click.tour-" + this._options.name, ".popover.tour-" + this._options.name + " *[data-role='pause-resume']", function(e) { - var $this; - e.preventDefault(); - $this = $(this); - $this.text(_this._paused ? $this.data("pause-text") : $this.data("resume-text")); - if (_this._paused) { - return _this.resume(); - } else { - return _this.pause(); - } - }); - }; - - Tour.prototype._initKeyboardNavigation = function() { - if (!this._options.keyboard) { - return; - } - return $(document).on("keyup.tour-" + this._options.name, (function(_this) { - return function(e) { - if (!e.which) { - return; - } - switch (e.which) { - case 39: - e.preventDefault(); - if (_this._isLast()) { - return _this.next(); - } else { - return _this.end(); - } - break; - case 37: - e.preventDefault(); - if (_this._current > 0) { - return _this.prev(); - } - break; - case 27: - e.preventDefault(); - return _this.end(); - } - }; - })(this)); - }; - - Tour.prototype._makePromise = function(result) { - if (result && $.isFunction(result.then)) { - return result; - } else { - return null; - } - }; - - Tour.prototype._callOnPromiseDone = function(promise, cb, arg) { - if (promise) { - return promise.then((function(_this) { - return function(e) { - return cb.call(_this, arg); - }; - })(this)); - } else { - return cb.call(this, arg); - } - }; - - Tour.prototype._showBackdrop = function(element) { - if (this.backdrop.backgroundShown) { - return; - } - this.backdrop = $("
", { - "class": "tour-backdrop" - }); - this.backdrop.backgroundShown = true; - return $("body").append(this.backdrop); - }; - - Tour.prototype._hideBackdrop = function() { - this._hideOverlayElement(); - return this._hideBackground(); - }; - - Tour.prototype._hideBackground = function() { - if (this.backdrop) { - this.backdrop.remove(); - this.backdrop.overlay = null; - return this.backdrop.backgroundShown = false; - } - }; - - Tour.prototype._showOverlayElement = function(element) { - var $background, $element, offset; - $element = $(element); - if (!$element || $element.length === 0 || this.backdrop.overlayElementShown) { - return; - } - this.backdrop.overlayElementShown = true; - $background = $("
"); - offset = $element.offset(); - offset.top = offset.top; - offset.left = offset.left; - $background.width($element.innerWidth()).height($element.innerHeight()).addClass("tour-step-background").offset(offset); - $element.addClass("tour-step-backdrop"); - $("body").append($background); - this.backdrop.$element = $element; - return this.backdrop.$background = $background; - }; - - Tour.prototype._hideOverlayElement = function() { - if (!this.backdrop.overlayElementShown) { - return; - } - this.backdrop.$element.removeClass("tour-step-backdrop"); - this.backdrop.$background.remove(); - this.backdrop.$element = null; - this.backdrop.$background = null; - return this.backdrop.overlayElementShown = false; - }; - - Tour.prototype._clearTimer = function() { - window.clearTimeout(this._timer); - this._timer = null; - return this._duration = null; - }; - - return Tour; - - })(); - return window.Tour = Tour; -})(jQuery, window); diff --git a/client/js/common/vendor/bootstrap.js b/client/js/common/vendor/bootstrap.js deleted file mode 100644 index 8779549c..00000000 --- a/client/js/common/vendor/bootstrap.js +++ /dev/null @@ -1,334 +0,0 @@ -// License: LGPL-3.0-or-later -/*! - * Bootstrap v3.3.2 (http://getbootstrap.com) - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - */ - -/*! - * Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=0d2d6ea77a31113c4876) - * Config saved to config.json and https://gist.github.com/0d2d6ea77a31113c4876 - */ -if (typeof jQuery === 'undefined') { - throw new Error('Bootstrap\'s JavaScript requires jQuery') -} -+function ($) { - 'use strict'; - var version = $.fn.jquery.split(' ')[0].split('.') - if ((version[0] < 2 && version[1] < 9) || (version[0] == 1 && version[1] == 9 && version[2] < 1)) { - throw new Error('Bootstrap\'s JavaScript requires jQuery version 1.9.1 or higher') - } -}(jQuery); - -/* ======================================================================== - * Bootstrap: carousel.js v3.3.2 - * http://getbootstrap.com/javascript/#carousel - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CAROUSEL CLASS DEFINITION - // ========================= - - var Carousel = function (element, options) { - this.$element = $(element) - this.$indicators = this.$element.find('.carousel-indicators') - this.options = options - this.paused = - this.sliding = - this.interval = - this.$active = - this.$items = null - - this.options.keyboard && this.$element.on('keydown.bs.carousel', $.proxy(this.keydown, this)) - - this.options.pause == 'hover' && !('ontouchstart' in document.documentElement) && this.$element - .on('mouseenter.bs.carousel', $.proxy(this.pause, this)) - .on('mouseleave.bs.carousel', $.proxy(this.cycle, this)) - } - - Carousel.VERSION = '3.3.2' - - Carousel.TRANSITION_DURATION = 600 - - Carousel.DEFAULTS = { - interval: 5000, - pause: 'hover', - wrap: true, - keyboard: true - } - - Carousel.prototype.keydown = function (e) { - if (/input|textarea/i.test(e.target.tagName)) return - switch (e.which) { - case 37: this.prev(); break - case 39: this.next(); break - default: return - } - - e.preventDefault() - } - - Carousel.prototype.cycle = function (e) { - e || (this.paused = false) - - this.interval && clearInterval(this.interval) - - this.options.interval - && !this.paused - && (this.interval = setInterval($.proxy(this.next, this), this.options.interval)) - - return this - } - - Carousel.prototype.getItemIndex = function (item) { - this.$items = item.parent().children('.item') - return this.$items.index(item || this.$active) - } - - Carousel.prototype.getItemForDirection = function (direction, active) { - var activeIndex = this.getItemIndex(active) - var willWrap = (direction == 'prev' && activeIndex === 0) - || (direction == 'next' && activeIndex == (this.$items.length - 1)) - if (willWrap && !this.options.wrap) return active - var delta = direction == 'prev' ? -1 : 1 - var itemIndex = (activeIndex + delta) % this.$items.length - return this.$items.eq(itemIndex) - } - - Carousel.prototype.to = function (pos) { - var that = this - var activeIndex = this.getItemIndex(this.$active = this.$element.find('.item.active')) - - if (pos > (this.$items.length - 1) || pos < 0) return - - if (this.sliding) return this.$element.one('slid.bs.carousel', function () { that.to(pos) }) // yes, "slid" - if (activeIndex == pos) return this.pause().cycle() - - return this.slide(pos > activeIndex ? 'next' : 'prev', this.$items.eq(pos)) - } - - Carousel.prototype.pause = function (e) { - e || (this.paused = true) - - if (this.$element.find('.next, .prev').length && $.support.transition) { - this.$element.trigger($.support.transition.end) - this.cycle(true) - } - - this.interval = clearInterval(this.interval) - - return this - } - - Carousel.prototype.next = function () { - if (this.sliding) return - return this.slide('next') - } - - Carousel.prototype.prev = function () { - if (this.sliding) return - return this.slide('prev') - } - - Carousel.prototype.slide = function (type, next) { - var $active = this.$element.find('.item.active') - var $next = next || this.getItemForDirection(type, $active) - var isCycling = this.interval - var direction = type == 'next' ? 'left' : 'right' - var that = this - - if ($next.hasClass('active')) return (this.sliding = false) - - var relatedTarget = $next[0] - var slideEvent = $.Event('slide.bs.carousel', { - relatedTarget: relatedTarget, - direction: direction - }) - this.$element.trigger(slideEvent) - if (slideEvent.isDefaultPrevented()) return - - this.sliding = true - - isCycling && this.pause() - - if (this.$indicators.length) { - this.$indicators.find('.active').removeClass('active') - var $nextIndicator = $(this.$indicators.children()[this.getItemIndex($next)]) - $nextIndicator && $nextIndicator.addClass('active') - } - - var slidEvent = $.Event('slid.bs.carousel', { relatedTarget: relatedTarget, direction: direction }) // yes, "slid" - if ($.support.transition && this.$element.hasClass('slide')) { - $next.addClass(type) - $next[0].offsetWidth // force reflow - $active.addClass(direction) - $next.addClass(direction) - $active - .one('bsTransitionEnd', function () { - $next.removeClass([type, direction].join(' ')).addClass('active') - $active.removeClass(['active', direction].join(' ')) - that.sliding = false - setTimeout(function () { - that.$element.trigger(slidEvent) - }, 0) - }) - .emulateTransitionEnd(Carousel.TRANSITION_DURATION) - } else { - $active.removeClass('active') - $next.addClass('active') - this.sliding = false - this.$element.trigger(slidEvent) - } - - isCycling && this.cycle() - - return this - } - - - // CAROUSEL PLUGIN DEFINITION - // ========================== - - function Plugin(option) { - return this.each(function () { - var $this = $(this) - var data = $this.data('bs.carousel') - var options = $.extend({}, Carousel.DEFAULTS, $this.data(), typeof option == 'object' && option) - var action = typeof option == 'string' ? option : options.slide - - if (!data) $this.data('bs.carousel', (data = new Carousel(this, options))) - if (typeof option == 'number') data.to(option) - else if (action) data[action]() - else if (options.interval) data.pause().cycle() - }) - } - - var old = $.fn.carousel - - $.fn.carousel = Plugin - $.fn.carousel.Constructor = Carousel - - - // CAROUSEL NO CONFLICT - // ==================== - - $.fn.carousel.noConflict = function () { - $.fn.carousel = old - return this - } - - - // CAROUSEL DATA-API - // ================= - - var clickHandler = function (e) { - var href - var $this = $(this) - var $target = $($this.attr('data-target') || (href = $this.attr('href')) && href.replace(/.*(?=#[^\s]+$)/, '')) // strip for ie7 - if (!$target.hasClass('carousel')) return - var options = $.extend({}, $target.data(), $this.data()) - var slideIndex = $this.attr('data-slide-to') - if (slideIndex) options.interval = false - - Plugin.call($target, options) - - if (slideIndex) { - $target.data('bs.carousel').to(slideIndex) - } - - e.preventDefault() - } - - $(document) - .on('click.bs.carousel.data-api', '[data-slide]', clickHandler) - .on('click.bs.carousel.data-api', '[data-slide-to]', clickHandler) - - $(window).on('load', function () { - $('[data-ride="carousel"]').each(function () { - var $carousel = $(this) - Plugin.call($carousel, $carousel.data()) - }) - }) - -}(jQuery); - - -/* ======================================================================== - * Bootstrap: transition.js v3.3.2 - * http://getbootstrap.com/javascript/#transitions - * ======================================================================== - * Copyright 2011-2015 Twitter, Inc. - * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) - * ======================================================================== */ - - -+function ($) { - 'use strict'; - - // CSS TRANSITION SUPPORT (Shoutout: http://www.modernizr.com/) - // ============================================================ - - function transitionEnd() { - var el = document.createElement('bootstrap') - - var transEndEventNames = { - WebkitTransition : 'webkitTransitionEnd', - MozTransition : 'transitionend', - OTransition : 'oTransitionEnd otransitionend', - transition : 'transitionend' - } - - for (var name in transEndEventNames) { - if (el.style[name] !== undefined) { - return { end: transEndEventNames[name] } - } - } - - return false // explicit for ie8 ( ._.) - } - - // http://blog.alexmaccaw.com/css-transitions - $.fn.emulateTransitionEnd = function (duration) { - var called = false - var $el = this - $(this).one('bsTransitionEnd', function () { called = true }) - var callback = function () { if (!called) $($el).trigger($.support.transition.end) } - setTimeout(callback, duration) - return this - } - - $(function () { - $.support.transition = transitionEnd() - - if (!$.support.transition) return - - $.event.special.bsTransitionEnd = { - bindType: $.support.transition.end, - delegateType: $.support.transition.end, - handle: function (e) { - if ($(e.target).is(this)) return e.handleObj.handler.apply(this, arguments) - } - } - }) - -}(jQuery); - - - - - -$(document).ready(function() { - setTimeout(function(){ - var $progressBar = $('#progress') - var width = $progressBar.attr('data-perc') - $progressBar.css('width', width + '%') - }, 1000) - }) - - diff --git a/client/js/common/vendor/colpick.js b/client/js/common/vendor/colpick.js deleted file mode 100644 index da3ed7c5..00000000 --- a/client/js/common/vendor/colpick.js +++ /dev/null @@ -1,517 +0,0 @@ -// License: LGPL-3.0-or-later -/* -colpick Color Picker -Copyright 2013 Jose Vargas. Licensed under GPL license. Based on Stefan Petre's Color Picker www.eyecon.ro, dual licensed under the MIT and GPL licenses - -For usage and examples: colpick.com/plugin - */ - -var colpick = function () { - var - tpl = '
#
R
G
B
H
S
B
', - defaults = { - showEvent: 'click', - onShow: function () {}, - onBeforeShow: function(){}, - onHide: function () {}, - onChange: function () {}, - onSubmit: function () {}, - colorScheme: 'light', - color: '3289c7', - livePreview: true, - flat: false, - layout: 'full', - submit: 1, - submitText: 'OK', - height: 156 - }, - //Fill the inputs of the plugin - fillRGBFields = function (hsb, cal) { - var rgb = hsbToRgb(hsb); - $(cal).data('colpick').fields - .eq(1).val(rgb.r).end() - .eq(2).val(rgb.g).end() - .eq(3).val(rgb.b).end(); - }, - fillHSBFields = function (hsb, cal) { - $(cal).data('colpick').fields - .eq(4).val(Math.round(hsb.h)).end() - .eq(5).val(Math.round(hsb.s)).end() - .eq(6).val(Math.round(hsb.b)).end(); - }, - fillHexFields = function (hsb, cal) { - $(cal).data('colpick').fields.eq(0).val(hsbToHex(hsb)); - }, - //Set the round selector position - setSelector = function (hsb, cal) { - $(cal).data('colpick').selector.css('backgroundColor', '#' + hsbToHex({h: hsb.h, s: 100, b: 100})); - $(cal).data('colpick').selectorIndic.css({ - left: parseInt($(cal).data('colpick').height * hsb.s/100, 10), - top: parseInt($(cal).data('colpick').height * (100-hsb.b)/100, 10) - }); - }, - //Set the hue selector position - setHue = function (hsb, cal) { - $(cal).data('colpick').hue.css('top', parseInt($(cal).data('colpick').height - $(cal).data('colpick').height * hsb.h/360, 10)); - }, - //Set current and new colors - setCurrentColor = function (hsb, cal) { - $(cal).data('colpick').currentColor.css('backgroundColor', '#' + hsbToHex(hsb)); - }, - setNewColor = function (hsb, cal) { - $(cal).data('colpick').newColor.css('backgroundColor', '#' + hsbToHex(hsb)); - }, - //Called when the new color is changed - change = function (ev) { - var cal = $(this).parent().parent(), col; - if (this.parentNode.className.indexOf('_hex') > 0) { - cal.data('colpick').color = col = hexToHsb(fixHex(this.value)); - fillRGBFields(col, cal.get(0)); - fillHSBFields(col, cal.get(0)); - } else if (this.parentNode.className.indexOf('_hsb') > 0) { - cal.data('colpick').color = col = fixHSB({ - h: parseInt(cal.data('colpick').fields.eq(4).val(), 10), - s: parseInt(cal.data('colpick').fields.eq(5).val(), 10), - b: parseInt(cal.data('colpick').fields.eq(6).val(), 10) - }); - fillRGBFields(col, cal.get(0)); - fillHexFields(col, cal.get(0)); - } else { - cal.data('colpick').color = col = rgbToHsb(fixRGB({ - r: parseInt(cal.data('colpick').fields.eq(1).val(), 10), - g: parseInt(cal.data('colpick').fields.eq(2).val(), 10), - b: parseInt(cal.data('colpick').fields.eq(3).val(), 10) - })); - fillHexFields(col, cal.get(0)); - fillHSBFields(col, cal.get(0)); - } - setSelector(col, cal.get(0)); - setHue(col, cal.get(0)); - setNewColor(col, cal.get(0)); - cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 0]); - }, - //Change style on blur and on focus of inputs - blur = function (ev) { - $(this).parent().removeClass('colpick_focus'); - }, - focus = function () { - $(this).parent().parent().data('colpick').fields.parent().removeClass('colpick_focus'); - $(this).parent().addClass('colpick_focus'); - }, - //Increment/decrement arrows functions - downIncrement = function (ev) { - ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; - var field = $(this).parent().find('input').focus(); - var current = { - el: $(this).parent().addClass('colpick_slider'), - max: this.parentNode.className.indexOf('_hsb_h') > 0 ? 360 : (this.parentNode.className.indexOf('_hsb') > 0 ? 100 : 255), - y: ev.pageY, - field: field, - val: parseInt(field.val(), 10), - preview: $(this).parent().parent().data('colpick').livePreview - }; - $(document).mouseup(current, upIncrement); - $(document).mousemove(current, moveIncrement); - }, - moveIncrement = function (ev) { - ev.data.field.val(Math.max(0, Math.min(ev.data.max, parseInt(ev.data.val - ev.pageY + ev.data.y, 10)))); - if (ev.data.preview) { - change.apply(ev.data.field.get(0), [true]); - } - return false; - }, - upIncrement = function (ev) { - change.apply(ev.data.field.get(0), [true]); - ev.data.el.removeClass('colpick_slider').find('input').focus(); - $(document).off('mouseup', upIncrement); - $(document).off('mousemove', moveIncrement); - return false; - }, - //Hue slider functions - downHue = function (ev) { - ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; - var current = { - cal: $(this).parent(), - y: $(this).offset().top - }; - $(document).on('mouseup touchend',current,upHue); - $(document).on('mousemove touchmove',current,moveHue); - - var pageY = ((ev.type == 'touchstart') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); - change.apply( - current.cal.data('colpick') - .fields.eq(4).val(parseInt(360*(current.cal.data('colpick').height - (pageY - current.y))/current.cal.data('colpick').height, 10)) - .get(0), - [current.cal.data('colpick').livePreview] - ); - return false; - }, - moveHue = function (ev) { - var pageY = ((ev.type == 'touchmove') ? ev.originalEvent.changedTouches[0].pageY : ev.pageY ); - change.apply( - ev.data.cal.data('colpick') - .fields.eq(4).val(parseInt(360*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.y))))/ev.data.cal.data('colpick').height, 10)) - .get(0), - [ev.data.preview] - ); - return false; - }, - upHue = function (ev) { - fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); - fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); - $(document).off('mouseup touchend',upHue); - $(document).off('mousemove touchmove',moveHue); - return false; - }, - //Color selector functions - downSelector = function (ev) { - ev.preventDefault ? ev.preventDefault() : ev.returnValue = false; - var current = { - cal: $(this).parent(), - pos: $(this).offset() - }; - current.preview = current.cal.data('colpick').livePreview; - - $(document).on('mouseup touchend',current,upSelector); - $(document).on('mousemove touchmove',current,moveSelector); - - if(ev.type == 'touchstart') { - var pageX = ev.originalEvent.changedTouches[0].pageX; - var pageY = ev.originalEvent.changedTouches[0].pageY; - } else { - var pageX = ev.pageX; - var pageY = ev.pageY; - } - - change.apply( - current.cal.data('colpick').fields - .eq(6).val(parseInt(100*(current.cal.data('colpick').height - (pageY - current.pos.top))/current.cal.data('colpick').height, 10)).end() - .eq(5).val(parseInt(100*(pageX - current.pos.left)/current.cal.data('colpick').height, 10)) - .get(0), - [current.preview] - ); - return false; - }, - moveSelector = function (ev) { - if(ev.type == 'touchmove') { - var pageX = ev.originalEvent.changedTouches[0].pageX; - var pageY = ev.originalEvent.changedTouches[0].pageY; - } else { - var pageX = ev.pageX; - var pageY = ev.pageY; - } - - change.apply( - ev.data.cal.data('colpick').fields - .eq(6).val(parseInt(100*(ev.data.cal.data('colpick').height - Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageY - ev.data.pos.top))))/ev.data.cal.data('colpick').height, 10)).end() - .eq(5).val(parseInt(100*(Math.max(0,Math.min(ev.data.cal.data('colpick').height,(pageX - ev.data.pos.left))))/ev.data.cal.data('colpick').height, 10)) - .get(0), - [ev.data.preview] - ); - return false; - }, - upSelector = function (ev) { - fillRGBFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); - fillHexFields(ev.data.cal.data('colpick').color, ev.data.cal.get(0)); - $(document).off('mouseup touchend',upSelector); - $(document).off('mousemove touchmove',moveSelector); - return false; - }, - //Submit button - clickSubmit = function (ev) { - var cal = $(this).parent(); - var col = cal.data('colpick').color; - cal.data('colpick').origColor = col; - setCurrentColor(col, cal.get(0)); - cal.data('colpick').onSubmit(col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el); - }, - //Show/hide the color picker - show = function (ev) { - // Prevent the trigger of any direct parent - ev.stopPropagation(); - var cal = $('#' + $(this).data('colpickId')); - cal.data('colpick').onBeforeShow.apply(this, [cal.get(0)]); - var pos = $(this).offset(); - var top = pos.top + this.offsetHeight; - var left = pos.left; - var viewPort = getViewport(); - var calW = cal.width(); - if (left + calW > viewPort.l + viewPort.w) { - left -= calW; - } - cal.css({left: left + 'px', top: top + 'px'}); - if (cal.data('colpick').onShow.apply(this, [cal.get(0)]) != false) { - cal.show(); - } - //Hide when user clicks outside - $('html').mousedown({cal:cal}, hide); - cal.mousedown(function(ev){ev.stopPropagation();}) - }, - hide = function (ev) { - if (ev.data.cal.data('colpick').onHide.apply(this, [ev.data.cal.get(0)]) != false) { - ev.data.cal.hide(); - } - $('html').off('mousedown', hide); - }, - getViewport = function () { - var m = document.compatMode == 'CSS1Compat'; - return { - l : window.pageXOffset || (m ? document.documentElement.scrollLeft : document.body.scrollLeft), - w : window.innerWidth || (m ? document.documentElement.clientWidth : document.body.clientWidth) - }; - }, - //Fix the values if the user enters a negative or high value - fixHSB = function (hsb) { - return { - h: Math.min(360, Math.max(0, hsb.h)), - s: Math.min(100, Math.max(0, hsb.s)), - b: Math.min(100, Math.max(0, hsb.b)) - }; - }, - fixRGB = function (rgb) { - return { - r: Math.min(255, Math.max(0, rgb.r)), - g: Math.min(255, Math.max(0, rgb.g)), - b: Math.min(255, Math.max(0, rgb.b)) - }; - }, - fixHex = function (hex) { - var len = 6 - hex.length; - if (len > 0) { - var o = []; - for (var i=0; i
').attr('style','height:8.333333%; filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+'); -ms-filter: "progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr='+stops[i]+', endColorstr='+stops[i+1]+')";'); - huebar.append(div); - } - } else { - var stopList = stops.join(','); - huebar.attr('style','background:-webkit-linear-gradient(top,'+stopList+'); background: -o-linear-gradient(top,'+stopList+'); background: -ms-linear-gradient(top,'+stopList+'); background:-moz-linear-gradient(top,'+stopList+'); -webkit-linear-gradient(top,'+stopList+'); background:linear-gradient(to bottom,'+stopList+'); '); - } - cal.find('div.colpick_hue').on('mousedown touchstart',downHue); - options.newColor = cal.find('div.colpick_new_color'); - options.currentColor = cal.find('div.colpick_current_color'); - //Store options and fill with default color - cal.data('colpick', options); - fillRGBFields(options.color, cal.get(0)); - fillHSBFields(options.color, cal.get(0)); - fillHexFields(options.color, cal.get(0)); - setHue(options.color, cal.get(0)); - setSelector(options.color, cal.get(0)); - setCurrentColor(options.color, cal.get(0)); - setNewColor(options.color, cal.get(0)); - //Append to body if flat=false, else show in place - if (options.flat) { - cal.appendTo(this).show(); - cal.css({ - position: 'relative', - display: 'block' - }); - } else { - cal.appendTo(document.body); - $(this).on(options.showEvent, show); - cal.css({ - position:'absolute' - }); - } - } - }); - }, - //Shows the picker - showPicker: function() { - return this.each( function () { - if ($(this).data('colpickId')) { - show.apply(this); - } - }); - }, - //Hides the picker - hidePicker: function() { - return this.each( function () { - if ($(this).data('colpickId')) { - $('#' + $(this).data('colpickId')).hide(); - } - }); - }, - //Sets a color as new and current (default) - setColor: function(col, setCurrent) { - setCurrent = (typeof setCurrent === "undefined") ? 1 : setCurrent; - if (typeof col == 'string') { - col = hexToHsb(col); - } else if (col.r != undefined && col.g != undefined && col.b != undefined) { - col = rgbToHsb(col); - } else if (col.h != undefined && col.s != undefined && col.b != undefined) { - col = fixHSB(col); - } else { - return this; - } - return this.each(function(){ - if ($(this).data('colpickId')) { - var cal = $('#' + $(this).data('colpickId')); - cal.data('colpick').color = col; - cal.data('colpick').origColor = col; - fillRGBFields(col, cal.get(0)); - fillHSBFields(col, cal.get(0)); - fillHexFields(col, cal.get(0)); - setHue(col, cal.get(0)); - setSelector(col, cal.get(0)); - - setNewColor(col, cal.get(0)); - cal.data('colpick').onChange.apply(cal.parent(), [col, hsbToHex(col), hsbToRgb(col), cal.data('colpick').el, 1]); - if(setCurrent) { - setCurrentColor(col, cal.get(0)); - } - } - }); - } - }; -}(); -//Color space convertions -var hexToRgb = function (hex) { - var hex = parseInt(((hex.indexOf('#') > -1) ? hex.substring(1) : hex), 16); - return {r: hex >> 16, g: (hex & 0x00FF00) >> 8, b: (hex & 0x0000FF)}; -}; -var hexToHsb = function (hex) { - return rgbToHsb(hexToRgb(hex)); -}; -var rgbToHsb = function (rgb) { - var hsb = {h: 0, s: 0, b: 0}; - var min = Math.min(rgb.r, rgb.g, rgb.b); - var max = Math.max(rgb.r, rgb.g, rgb.b); - var delta = max - min; - hsb.b = max; - hsb.s = max != 0 ? 255 * delta / max : 0; - if (hsb.s != 0) { - if (rgb.r == max) hsb.h = (rgb.g - rgb.b) / delta; - else if (rgb.g == max) hsb.h = 2 + (rgb.b - rgb.r) / delta; - else hsb.h = 4 + (rgb.r - rgb.g) / delta; - } else hsb.h = -1; - hsb.h *= 60; - if (hsb.h < 0) hsb.h += 360; - hsb.s *= 100/255; - hsb.b *= 100/255; - return hsb; -}; -var hsbToRgb = function (hsb) { - var rgb = {}; - var h = hsb.h; - var s = hsb.s*255/100; - var v = hsb.b*255/100; - if(s == 0) { - rgb.r = rgb.g = rgb.b = v; - } else { - var t1 = v; - var t2 = (255-s)*v/255; - var t3 = (t1-t2)*(h%60)/60; - if(h==360) h = 0; - if(h<60) {rgb.r=t1; rgb.b=t2; rgb.g=t2+t3} - else if(h<120) {rgb.g=t1; rgb.b=t2; rgb.r=t1-t3} - else if(h<180) {rgb.g=t1; rgb.r=t2; rgb.b=t2+t3} - else if(h<240) {rgb.b=t1; rgb.r=t2; rgb.g=t1-t3} - else if(h<300) {rgb.b=t1; rgb.g=t2; rgb.r=t2+t3} - else if(h<360) {rgb.r=t1; rgb.g=t2; rgb.b=t1-t3} - else {rgb.r=0; rgb.g=0; rgb.b=0} - } - return {r:Math.round(rgb.r), g:Math.round(rgb.g), b:Math.round(rgb.b)}; -}; -var rgbToHex = function (rgb) { - var hex = [ - rgb.r.toString(16), - rgb.g.toString(16), - rgb.b.toString(16) - ]; - $.each(hex, function (nr, val) { - if (val.length == 1) { - hex[nr] = '0' + val; - } - }); - return hex.join(''); -}; -var hsbToHex = function (hsb) { - return rgbToHex(hsbToRgb(hsb)); -}; -$.fn.extend({ - colpick: colpick.init, - colpickHide: colpick.hidePicker, - colpickShow: colpick.showPicker, - colpickSetColor: colpick.setColor -}); -$.extend({ - colpick:{ - rgbToHex: rgbToHex, - rgbToHsb: rgbToHsb, - hsbToHex: hsbToHex, - hsbToRgb: hsbToRgb, - hexToHsb: hexToHsb, - hexToRgb: hexToRgb - } -}); diff --git a/client/js/common/vendor/jquery.cookie.js b/client/js/common/vendor/jquery.cookie.js deleted file mode 100755 index f20f9e38..00000000 --- a/client/js/common/vendor/jquery.cookie.js +++ /dev/null @@ -1,110 +0,0 @@ -// License: LGPL-3.0-or-later -/*! - * jQuery Cookie Plugin v1.4.1 - * https://github.com/carhartl/jquery-cookie - * - * Copyright 2013 Klaus Hartl - * Released under the MIT license - */ -(function (factory) { - factory(jQuery); -}(function ($) { - - var pluses = /\+/g; - - function encode(s) { - return config.raw ? s : encodeURIComponent(s); - } - - function decode(s) { - return config.raw ? s : decodeURIComponent(s); - } - - function stringifyCookieValue(value) { - return encode(config.json ? JSON.stringify(value) : String(value)); - } - - function parseCookieValue(s) { - if (s.indexOf('"') === 0) { - // This is a quoted cookie as according to RFC2068, unescape... - s = s.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, '\\'); - } - - try { - // Replace server-side written pluses with spaces. - // If we can't decode the cookie, ignore it, it's unusable. - // If we can't parse the cookie, ignore it, it's unusable. - s = decodeURIComponent(s.replace(pluses, ' ')); - return config.json ? JSON.parse(s) : s; - } catch(e) {} - } - - function read(s, converter) { - var value = config.raw ? s : parseCookieValue(s); - return $.isFunction(converter) ? converter(value) : value; - } - - var config = $.cookie = function (key, value, options) { - - // Write - - if (value !== undefined && !$.isFunction(value)) { - options = $.extend({}, config.defaults, options); - - if (typeof options.expires === 'number') { - var days = options.expires, t = options.expires = new Date(); - t.setTime(+t + days * 864e+5); - } - - return (document.cookie = [ - encode(key), '=', stringifyCookieValue(value), - options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE - options.path ? '; path=' + options.path : '', - options.domain ? '; domain=' + options.domain : '', - options.secure ? '; secure' : '' - ].join('')); - } - - // Read - - var result = key ? undefined : {}; - - // To prevent the for loop in the first place assign an empty array - // in case there are no cookies at all. Also prevents odd result when - // calling $.cookie(). - var cookies = document.cookie ? document.cookie.split('; ') : []; - - for (var i = 0, l = cookies.length; i < l; i++) { - var parts = cookies[i].split('='); - var name = decode(parts.shift()); - var cookie = parts.join('='); - - if (key && key === name) { - // If second argument (value) is a function it's a converter... - result = read(cookie, value); - break; - } - - // Prevent storing a cookie that we couldn't decode. - if (!key && (cookie = read(cookie)) !== undefined) { - result[name] = cookie; - } - } - - return result; - }; - - config.defaults = {}; - - - $.removeCookie = function (key, options) { - if ($.cookie(key) === undefined) { - return false; - } - - // Must not alter options, thus extending a fresh object... - $.cookie(key, '', $.extend({}, options, { expires: -1 })); - return !$.cookie(key); - }; - -})); diff --git a/client/js/common/vendor/masonry.js b/client/js/common/vendor/masonry.js deleted file mode 100644 index 1d312791..00000000 --- a/client/js/common/vendor/masonry.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -/*! - * Salvattore 1.0.8 by @rnmp and @ppold - * https://github.com/rnmp/salvattore - */ -!function(e,t){"function"==typeof define&&define.amd?define([],t):"object"==typeof exports?module.exports=t():e.salvattore=t()}(this,function(){/*! matchMedia() polyfill - Test a CSS media type/query in JS. Authors & copyright (c) 2012: Scott Jehl, Paul Irish, Nicholas Zakas, David Knight. Dual MIT/BSD license */ -window.matchMedia||(window.matchMedia=function(){"use strict";var e=window.styleMedia||window.media;if(!e){var t=document.createElement("style"),n=document.getElementsByTagName("script")[0],r=null;t.type="text/css",t.id="matchmediajs-test",n.parentNode.insertBefore(t,n),r="getComputedStyle"in window&&window.getComputedStyle(t,null)||t.currentStyle,e={matchMedium:function(e){var n="@media "+e+"{ #matchmediajs-test { width: 1px; } }";return t.styleSheet?t.styleSheet.cssText=n:t.textContent=n,"1px"===r.width}}}return function(t){return{matches:e.matchMedium(t||"all"),media:t||"all"}}}()),/*! matchMedia() polyfill addListener/removeListener extension. Author & copyright (c) 2012: Scott Jehl. Dual MIT/BSD license */ -function(){"use strict";if(window.matchMedia&&window.matchMedia("all").addListener)return!1;var e=window.matchMedia,t=e("only all").matches,n=!1,r=0,a=[],i=function(t){clearTimeout(r),r=setTimeout(function(){for(var t=0,n=a.length;n>t;t++){var r=a[t].mql,i=a[t].listeners||[],o=e(r.media).matches;if(o!==r.matches){r.matches=o;for(var c=0,l=i.length;l>c;c++)i[c].call(window,r)}}},30)};window.matchMedia=function(r){var o=e(r),c=[],l=0;return o.addListener=function(e){t&&(n||(n=!0,window.addEventListener("resize",i,!0)),0===l&&(l=a.push({mql:o,listeners:c})),c.push(e))},o.removeListener=function(e){for(var t=0,n=c.length;n>t;t++)c[t]===e&&c.splice(t,1)},o}}(),function(){"use strict";for(var e=0,t=["ms","moz","webkit","o"],n=0;n *:nth-child("+o+"n-"+d+")",s.push(n.querySelectorAll(a));s.forEach(function(e){var n=t.createElement("div"),r=t.createDocumentFragment();n.className=l.join(" "),Array.prototype.forEach.call(e,function(e){r.appendChild(e)}),n.appendChild(r),u.appendChild(n)}),e.appendChild(u),c(e,"columns",o)},r.removeColumns=function(n){var r=t.createRange();r.selectNodeContents(n);var a=Array.prototype.filter.call(r.extractContents().childNodes,function(t){return t instanceof e.HTMLElement}),i=a.length,o=a[0].childNodes.length,l=new Array(o*i);Array.prototype.forEach.call(a,function(e,t){Array.prototype.forEach.call(e.children,function(e,n){l[n*i+t]=e})});var s=t.createElement("div");return c(s,"columns",0),l.filter(function(e){return!!e}).forEach(function(e){s.appendChild(e)}),s},r.recreateColumns=function(t){e.requestAnimationFrame(function(){r.addColumns(t,r.removeColumns(t));var e=new CustomEvent("columnsChange");t.dispatchEvent(e)})},r.mediaQueryChange=function(e){e.matches&&Array.prototype.forEach.call(a,r.recreateColumns)},r.getCSSRules=function(e){var t;try{t=e.sheet.cssRules||e.sheet.rules}catch(n){return[]}return t||[]},r.getStylesheets=function(){var e=Array.prototype.slice.call(t.querySelectorAll("style"));return e.forEach(function(t,n){"text/css"!==t.type&&""!==t.type&&e.splice(n,1)}),Array.prototype.concat.call(e,Array.prototype.slice.call(t.querySelectorAll("link[rel='stylesheet']")))},r.mediaRuleHasColumnsSelector=function(e){var t,n;try{t=e.length}catch(r){t=0}for(;t--;)if(n=e[t],n.selectorText&&n.selectorText.match(/\[data-columns\](.*)::?before$/))return!0;return!1},r.scanMediaQueries=function(){var t=[];if(e.matchMedia){r.getStylesheets().forEach(function(e){Array.prototype.forEach.call(r.getCSSRules(e),function(e){try{e.media&&e.cssRules&&r.mediaRuleHasColumnsSelector(e.cssRules)&&t.push(e)}catch(n){}})});var n=i.filter(function(e){return-1===t.indexOf(e)});o.filter(function(e){return-1!==n.indexOf(e.rule)}).forEach(function(e){e.mql.removeListener(r.mediaQueryChange)}),o=o.filter(function(e){return-1===n.indexOf(e.rule)}),t.filter(function(e){return-1==i.indexOf(e)}).forEach(function(t){var n=e.matchMedia(t.media.mediaText);n.addListener(r.mediaQueryChange),o.push({rule:t,mql:n})}),i.length=0,i=t}},r.rescanMediaQueries=function(){r.scanMediaQueries(),Array.prototype.forEach.call(a,r.recreateColumns)},r.nextElementColumnIndex=function(e,t){var n,r,a,i=e.children,o=i.length,c=0,l=0;for(a=0;o>a;a++)n=i[a],r=n.children.length+(t[a].children||t[a].childNodes).length,0===c&&(c=r),c>r&&(l=a,c=r);return l},r.createFragmentsList=function(e){for(var n=new Array(e),r=0;r!==e;)n[r]=t.createDocumentFragment(),r++;return n},r.appendElements=function(e,t){var n=e.children,a=n.length,i=r.createFragmentsList(a);Array.prototype.forEach.call(t,function(t){var n=r.nextElementColumnIndex(e,i);i[n].appendChild(t)}),Array.prototype.forEach.call(n,function(e,t){e.appendChild(i[t])})},r.prependElements=function(e,n){var a=e.children,i=a.length,o=r.createFragmentsList(i),c=i-1;n.forEach(function(e){var t=o[c];t.insertBefore(e,t.firstChild),0===c?c=i-1:c--}),Array.prototype.forEach.call(a,function(e,t){e.insertBefore(o[t],e.firstChild)});for(var l=t.createDocumentFragment(),s=n.length%i;0!==s--;)l.appendChild(e.lastChild);e.insertBefore(l,e.firstChild)},r.registerGrid=function(n){if("none"!==e.getComputedStyle(n).display){var i=t.createRange();i.selectNodeContents(n);var o=t.createElement("div");o.appendChild(i.extractContents()),c(o,"columns",0),r.addColumns(n,o),a.push(n)}},r.init=function(){var e=t.createElement("style");e.innerHTML="[data-columns]::before{display:block;visibility:hidden;position:absolute;font-size:1px;}",t.head.appendChild(e);var n=t.querySelectorAll("[data-columns]");Array.prototype.forEach.call(n,r.registerGrid),r.scanMediaQueries()},r.init(),{appendElements:r.appendElements,prependElements:r.prependElements,registerGrid:r.registerGrid,recreateColumns:r.recreateColumns,rescanMediaQueries:r.rescanMediaQueries,init:r.init,append_elements:r.appendElements,prepend_elements:r.prependElements,register_grid:r.registerGrid,recreate_columns:r.recreateColumns,rescan_media_queries:r.rescanMediaQueries}}(window,window.document);return e}); - diff --git a/client/js/components/activity_feed.js b/client/js/components/activity_feed.js deleted file mode 100644 index 10733a7e..00000000 --- a/client/js/components/activity_feed.js +++ /dev/null @@ -1,3 +0,0 @@ -// License: LGPL-3.0-or-later - - diff --git a/client/js/components/address-autocomplete-fields.js b/client/js/components/address-autocomplete-fields.js deleted file mode 100644 index 596783fd..00000000 --- a/client/js/components/address-autocomplete-fields.js +++ /dev/null @@ -1,96 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const h = require('snabbdom/h') -const R = require('ramda') -flyd.lift = require('flyd/module/lift') - - - -function init(state, params$) { - state = state||{} - state = R.merge({ - isManual$: flyd.stream(false) - , data$: flyd.stream(app.profile ? R.pick(['address', 'city', 'state_code', 'zip_code'], app.profile) : {}) - , autocompleteInputInserted$: flyd.stream() - }, state) - - state.isManual$ = flyd.stream(true) - state.params$ = params$ - return state -} - -function calculateToShip(state) -{ - return state.params$().gift_option && state.params$().gift_option.to_ship -} - -function view(state) { - return h('section.u-padding--5.pastelBox--grey clearfix', [ - calculateToShip(state) - ? h('label.u-centered.u-marginBottom--5', [ - 'Shipping address (required)' - ]) - : '' - , manualFields(state) - ]) -} - -const manualFields = state => { - return h('div', [ - h('fieldset.col-8.u-fontSize--14', [ - h('input.u-marginBottom--0', {props: { - type: 'text' - , title: 'Street Addresss' - , name: 'address' - , placeholder: 'Street Address' - , value: state.data$().address - , required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined - }}) - ]) - , h('fieldset.col-right-4.u-fontSize--15', [ - h('input.u-marginBottom--0', {props: { - type: 'text' - , name: 'city' - , title: 'City' - , placeholder: 'City' - , value: state.data$().city - , required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined - }}) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-4', [ - h('input.u-marginBottom--0', {props: { - type: 'text' - , name: 'state_code' - , title: 'State/Region' - , placeholder: 'State/Region' - , value: state.data$().state_code - , required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined - }}) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-4.u-fontSize--14', [ - h('input.u-marginBottom--0', {props: { - type: 'text' - , title: 'Zip/Postal' - , name: 'zip_code' - , placeholder: 'Zip/Postal' - , value: state.data$().zip_code - , required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined - }}) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-4', [ - h('input.u-marginBottom--0', {props: { - type: 'text' - , title: 'Country' - , name: 'country' - , placeholder: 'Country' - , value: state.data$().country - , required: calculateToShip(state) ? state.params$().gift_option.to_ship : undefined - }}) - ]) - ]) -} - - -module.exports = {init, view} - - diff --git a/client/js/components/address-autocomplete.js b/client/js/components/address-autocomplete.js deleted file mode 100644 index dd756057..00000000 --- a/client/js/components/address-autocomplete.js +++ /dev/null @@ -1,81 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const flyd = require('flyd') - -// Stream that has true when google script is loaded -const loaded$ = flyd.stream() -// Stream of autocomplete data -const data$ = flyd.stream() - -function initScript() { - // if(document.getElementById('googleAutocomplete')) return - // var script = document.createElement('script') - // script.type = 'text/javascript' - // script.id = 'googleAutocomplete' - // document.body.appendChild(script) - // script.src = `https://maps.googleapis.com/maps/api/js?key=${app.google_api}&libraries=places&callback=initGoogleAutocomplete` - return loaded$ -} - -window.initGoogleAutocomplete = () => loaded$(true) - -function initInput(input) { - var autocomplete = new google.maps.places.Autocomplete(input, {types: ['geocode']}) - autocomplete.addListener('place_changed', fillInAddress(autocomplete)) - input.addEventListener('focus', geolocate(autocomplete)) - input.addEventListener('keydown', e => { if(e.which === 13) e.preventDefault() }) - return data$ -} - -const acceptedTypes = { - street_number: 'short_name' -, route: 'long_name' -, locality: 'long_name' -, administrative_area_level_1: 'short_name' -, country: 'long_name' -, postal_code: 'short_name' -} - -const fillInAddress = autocomplete => () => { - var place = { components: autocomplete.getPlace().address_components} - if(!place.components) return - place.types = R.map(x => x.types[0], place.components) - var address = placeData(place, 'street_number') - ? placeData(place, 'street_number') + ' ' + placeData(place, 'route') - : '' - - var data = { - address: address - , city: placeData(place, 'locality') - , state_code: placeData(place, 'administrative_area_level_1') - , country: placeData(place, 'country') - , zip_code: placeData(place, 'postal_code') - } - data$(data) -} - -function placeData(place, key) { - const i = R.findIndex(R.equals(key), place.types) - if(i >= 0) return place.components[i][acceptedTypes[key]] - return '' -} - -// Bias the autocomplete object to the user's geographical location, -// as supplied by the browser's 'navigator.geolocation' object. -const geolocate = autocomplete => () => { - if(!navigator || !navigator.geolocation) return - navigator.geolocation.getCurrentPosition(pos => { - var geolocation = { - lat: pos.coords.latitude - , lng: pos.coords.longitude - } - var circle = new google.maps.Circle({ - center: geolocation - , radius: pos.coords.accuracy - }) - autocomplete.setBounds(circle.getBounds()) - }) -} - - -module.exports = {initScript, initInput, data$, loaded$} diff --git a/client/js/components/ajax/toggle_soft_delete.js b/client/js/components/ajax/toggle_soft_delete.js deleted file mode 100644 index ab644013..00000000 --- a/client/js/components/ajax/toggle_soft_delete.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/client') - -module.exports = function(url, type) { - appl.def('toggle_soft_delete', function(bool) { - appl.def('loading', true) - var action = bool ? 'deleted.' : 'undeleted.' - request.put(url + '/soft_delete', {delete: bool}).end(function(err, resp) { - appl.def('loading', false) - .def(type + '_is_deleted', bool) - .notify('Successfully ' + action) - .close_modal() - }) - }) -} diff --git a/client/js/components/b64.js b/client/js/components/b64.js deleted file mode 100644 index d7ec4219..00000000 --- a/client/js/components/b64.js +++ /dev/null @@ -1,13 +0,0 @@ -// License: LGPL-3.0-or-later -// see https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding -// used for encoded and decoding data for email text - -module.exports = { - encode: str => - btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g - , (match, p1) => String.fromCharCode('0x' + p1))).replace(/\//g,'_').replace(/\+/g,'-') - , decode: str => - decodeURIComponent(Array.prototype.map.call(atob(str.replace(/-/g, '+').replace(/_/g, '/')) - , c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)).join('')) -} - diff --git a/client/js/components/branded_fundraising.js b/client/js/components/branded_fundraising.js deleted file mode 100644 index efe857c8..00000000 --- a/client/js/components/branded_fundraising.js +++ /dev/null @@ -1,10 +0,0 @@ -// License: LGPL-3.0-or-later -const brandColors = require('../components/nonprofit-branding') - -$('[if-branded]').each(function() { - var params = this.getAttribute("if-branded").split(',').map(function(s) { return s.trim() }) - $(this).css(params[0], brandColors[params[1]]) -}) - -exports = brandColors - diff --git a/client/js/components/card-form.es6 b/client/js/components/card-form.es6 deleted file mode 100644 index 83a40a19..00000000 --- a/client/js/components/card-form.es6 +++ /dev/null @@ -1,190 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const h = require('snabbdom/h') -const R = require('ramda') -const validatedForm = require('ff-core/validated-form') -const button = require('ff-core/button') -const flyd = require('flyd') -flyd.flatMap = require('flyd/module/flatmap') -flyd.filter = require('flyd/module/filter') -flyd.mergeAll = require('flyd/module/mergeall') -const scanMerge = require('flyd/module/scanmerge') -// local -const request = require('../common/request') -const formatErr = require('../common/format_response_error') -const createCardStream = require('../cards/create-frp.es6') -const serializeForm = require('form-serialize') -const luhnCheck = require('../common/credit-card-validator.js') - -// A component for filling out card data, validating it, saving the card to -// stripe, and then saving a tokenized copy to our servers. - -// Form validation constraints, validator functions, and error messages: -var constraints = { - address_zip: {required: true} -, name: {required: true} -, number: {required: true, cardNumber: true} -, exp_month: {required: true, format: /\d\d?/} -, exp_year: {required: true, format: /\d\d?/} -, cvc: {required: true, format: /\d\d\d\d?/} -} -var validators = { cardNumber: luhnCheck } -var messages = { - number: { - required: I18n.t('nonprofits.donate.payment.card.errors.number.presence') - , cardNumber: I18n.t('nonprofits.donate.payment.card.errors.number.format') - } - , required: I18n.t('nonprofits.donate.payment.card.errors.field.presence') - , email: I18n.t('nonprofits.donate.payment.card.errors.email.format') - , format: I18n.t('nonprofits.donate.payment.card.errors.field.format') -} - -// You can pass in the .hideButton boolean if you want to control whether the submit button is shown/hidden -// Pass in a .card object, which can have default objects for the card form (name, number, cvc, exp_month, etc) -// Pass in .path to set the endpoint for saving the card -// Pass in .payload for default data to send to the server for every card save request (such as a request token) -const init = (state) => { - state = state || {} - // set defaults - state = R.merge({ - payload$: flyd.stream(state.payload || {}) - , path$: flyd.stream(state.path || '/cards') - }, state) - - state.form = validatedForm.init({constraints, validators, messages}) - state.card$ = flyd.merge(flyd.stream(state.card || {}), state.form.validData$) - - // streams of stripe tokenization responses - const stripeResp$ = flyd.flatMap(createCardStream, state.form.validData$) - state.stripeRespOk$ = flyd.filter(r => !r.error, stripeResp$) - const stripeError$ = flyd.map(r => r.error.message, flyd.filter(r => r.error, stripeResp$)) - - // Save the card as a card table on our own db - // streams of responses - state.resp$ = flyd.flatMap( - resp => saveCard(state.payload$(), state.path$(), resp) // cheating on the streams here.. - , state.stripeRespOk$ ) - - const ccError$ = flyd.map(R.prop('error'), flyd.filter(resp => resp.error, state.resp$)) - state.saved$ = flyd.filter(resp => !resp.error, state.resp$) - state.error$ = flyd.merge(stripeError$, ccError$) - - state.loading$ = scanMerge([ - [state.form.validSubmit$, R.always(true)] - , [state.error$, R.always(false)] - , [state.saved$, R.always(false)] - ], false) - - return state -} - - -// -- Stream-related functions - -// Save the card to our own servers, and return a response stream -const saveCard = (send, path, resp) => { - send.card = R.merge(send.card, { - cardholders_name: resp.name - , name: `${resp.card.brand} *${resp.card.last4}` - , stripe_card_token: resp.id - , stripe_card_id: resp.card.id - }) - return flyd.map(R.prop('body'), request({ path, send, method: 'post' }).load) -} - - -// -- Virtual DOM - -const view = state => { - var field = validatedForm.field(state.form) - return validatedForm.form(state.form, h('form.cardForm', [ - h('div.u-background--grey.group.u-padding--8', [ - nameInput(field, state.card$().name) - , numberInput(field) - , cvcInput(field) - , expMonthInput(field) - , expYearInput(field) - , zipInput(field, state.card$().address_zip) - , profileInput(field, app.profile_id) // XXX global - ]) - , h('div.u-centered.u-marginTop--20', [ - state.hideButton ? '' : button({ - error$: state.hideErrors ? flyd.stream() : state.error$ - , loading$: state.loading$ - , buttonText: I18n.t('nonprofits.donate.payment.card.submit') - , loadingText: ` ${I18n.t('nonprofits.donate.payment.card.loading')}` - }) - , h('p.u-fontSize--12.u-marginBottom--0.u-marginTop--10.u-color--grey', [ h('i.fa.fa-lock'), ` ${I18n.t('nonprofits.donate.payment.card.secure_info')}`]) - ]) - ]) ) -} - - -const nameInput = (field, name) => - h('fieldset', [ field(h('input', { props: { name: 'name' , value: name || '', placeholder: I18n.t('nonprofits.donate.payment.card.name') } })) ]) - - -const numberInput = field => - h('fieldset.col-8', [ field(h('input', {props: { type: 'text' , name: 'number' , placeholder: I18n.t('nonprofits.donate.payment.card.number') } })) ]) - - -const cvcInput = field => - h('fieldset.col-right-4.u-relative', [ - field(h('input', { props: { name: 'cvc' , placeholder: I18n.t('nonprofits.donate.payment.card.cvc') } } )) - , h('img.security-code-image', { - src: `${app.asset_path}/graphics/cc-security-code.png` - }) - ]) - - -const expMonthInput = field => { - var options = R.prepend( - h('option.default', {props: {value: undefined, selected: true}}, I18n.t('nonprofits.donate.payment.card.month')) - , R.range(1, 13).map(n => h('option', String(n))) - ) - return h('fieldset.col-3.u-margin--0', [ - field(h('select.select' - , { props: {name: 'exp_month'} } - , options)) - ]) -} - - -const expYearInput = field => { - var yearRange = R.range(new Date().getFullYear(), new Date().getFullYear() + 15) - var options = R.prepend( - h('option.default', {props: {value: undefined, selected: true}}, I18n.t('nonprofits.donate.payment.card.year')) - , R.map(y => h('option', String(y)), yearRange) - ) - return h('fieldset.col-left-3.u-margin--0', [ - field(h('select.select' - , {props: {name: 'exp_year'}} - , options)) - ]) -} - - -const zipInput = (field, zip) => - h('fieldset.col-right-6.u-margin--0', [ - field(h('input' - , { props: { - type: 'text' - , name: 'address_zip' - , value: zip || '' - , placeholder: I18n.t('nonprofits.donate.payment.card.postal_code') - }} - )) - ]) - - -const profileInput = (field, profile_id) => - field(h('input' - , { props: { - type: 'hidden' - , name: 'profile_id' - , value: profile_id || '' - }} - )) - -module.exports = {view, init} - diff --git a/client/js/components/chart-options.js b/client/js/components/chart-options.js deleted file mode 100644 index 77c0b1c3..00000000 --- a/client/js/components/chart-options.js +++ /dev/null @@ -1,29 +0,0 @@ -// License: LGPL-3.0-or-later -var chartOptions = {} - -chartOptions.default = { - defaultFontFamily: "'Open Sans', 'Helvetica Neue', 'Arial', 'sans-serif'" -, scales: { - yAxes: [{ ticks: { min: 0 }}] - } -} - -chartOptions.dollars = { - defaultFontFamily: "'Open Sans', 'Helvetica Neue', 'Arial', 'sans-serif'" -, scales: { - yAxes: [{ ticks: { - min: 0 - , callback: (val) => '$' + utils.cents_to_dollars(val) - } }] - } -, tooltips: { - callbacks: { - label: (item, data) => - data.datasets[item.datasetIndex].label + - ': $' + utils.cents_to_dollars(item.yLabel) - } - } -} - -module.exports = chartOptions - diff --git a/client/js/components/checkbox.js b/client/js/components/checkbox.js deleted file mode 100644 index 42ff14b4..00000000 --- a/client/js/components/checkbox.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const uuid = require('uuid') - -// example: -// checkbox({name: 'anonymous', value: 'true', label: 'Donate anonymously?'}) - -module.exports = obj => { - const id = uuid.v1() - return h('div', [ - h('input', {props: {type: 'checkbox', id, value: obj.value, name: obj.name}}) - , h('label', {attrs: {for: id}}, [h('span.pl-1.sub.font-weight-1', obj.label ? obj.label : obj.value)]) - ]) -} - diff --git a/client/js/components/color-picker.es6 b/client/js/components/color-picker.es6 deleted file mode 100644 index 49fa058b..00000000 --- a/client/js/components/color-picker.es6 +++ /dev/null @@ -1,32 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const flyd = require('flyd') -const R = require('ramda') -require('../common/vendor/colpick') // XXX jquery - -// Color picker UI component, wrapping the colpick jquery plugin -// You can use colorPicker.streams.color to access a stream of hex color values selected by the user -// Will also set colorPicker.state.color for every selected color value - -function init(defaultColor) { - var logoBlue = '#42B3DF' - return {color$: flyd.stream(defaultColor || logoBlue)} -} - -const view = state => - h('div.colPick-wrapper.inner#colorpicker', { - hook: { - insert: (vnode) => { - $(vnode.elm).colpick({ - flat: true - , layout: 'hex' - , submit: false - , color: state.color$() - , onChange: (hsb, hex, rgb, el, bySetColor) => state.color$('#' + hex) - }) - } - } - }) - -module.exports = {init, view} - diff --git a/client/js/components/confirmation-modal.js b/client/js/components/confirmation-modal.js deleted file mode 100644 index cd867a2c..00000000 --- a/client/js/components/confirmation-modal.js +++ /dev/null @@ -1,44 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const uuid = require('uuid') -const flyd = require('flyd') -const modal = require('ff-core/modal') -const mergeAll = require('flyd/module/mergeall') - -// show$ is the stream that shows the confirmation modal -const init = show$ => { - const state = { - confirm$: flyd.stream() - , unconfirm$: flyd.stream() - , ID: uuid.v1() - } - - state.modalID$ = mergeAll([ - flyd.map(R.always(state.ID), show$) - , flyd.map(R.always(null), state.unconfirm$) - , flyd.map(R.always(null), state.confirm$)]) - - return state -} - -// msg is optional -const view = (state, msg) => - modal({ - id$: state.modalID$ - , thisID: state.ID - , notCloseable: true - , body: h('div', [ - h('h4', msg || 'Are you sure?') - , h('div', [ - h('button', {attrs: {'data-ff-confirm': true}, on: {click: state.confirm$}} - , 'Yes') - , h('button', {attrs: {'data-ff-confirm': false}, on: {click: state.unconfirm$}} - , 'No') - ]) - ]) - }) - - -module.exports = {init, view} - diff --git a/client/js/components/date-range.js b/client/js/components/date-range.js deleted file mode 100644 index f7873a9d..00000000 --- a/client/js/components/date-range.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -const moment = require('moment') -require('moment-range') - -// returns an array of moments -// timeSpan is one of 'day, 'week', 'month', 'year' (see moment.js docs) -module.exports = (startDate, endDate, timeSpan) => { - var dates = [moment(startDate), moment(endDate)] - return moment.range(dates).toArray(timeSpan) -} - diff --git a/client/js/components/date_range_picker.js b/client/js/components/date_range_picker.js deleted file mode 100644 index e0752fe4..00000000 --- a/client/js/components/date_range_picker.js +++ /dev/null @@ -1,32 +0,0 @@ -// License: LGPL-3.0-or-later -var Pikaday = require('pikaday') -var moment = require('moment') - -var el = document.querySelector('#dateRange') -if(el) { - var before_date = el.querySelector('#beforeDate') - var after_date = el.querySelector('#afterDate') -} - -function format_date(el) { - return function(date) { - el.value = moment(date).format('MM/DD/YYYY') - } -} - -if(el && before_date) { - new Pikaday({ - field: before_date, - format: 'MM/DD/YYYY', - onSelect: format_date(before_date) - }) -} - -if(el && after_date) { - new Pikaday({ - field: after_date, - format: 'MM/DD/YYYY', - onSelect: format_date(after_date) - }) -} - diff --git a/client/js/components/dollar-input.js b/client/js/components/dollar-input.js deleted file mode 100644 index 86fde2a9..00000000 --- a/client/js/components/dollar-input.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') - -module.exports = (name, placeholder, value) => { -return h('input.dollar-input.max-width-2', { - props: { - type: 'number' - , step: 'any' - , min: 0 - , name - , placeholder - , value - } - }) -} diff --git a/client/js/components/drag-to-reorder.js b/client/js/components/drag-to-reorder.js deleted file mode 100644 index 04348eb6..00000000 --- a/client/js/components/drag-to-reorder.js +++ /dev/null @@ -1,37 +0,0 @@ -// License: LGPL-3.0-or-later -const dragula = require('dragula') -const serialize = require('form-serialize') -const R = require('ramda') -const request = require('../common/request') -const flyd = require('flyd') -const flatMap = require('flyd/module/flatmap') - -const mapIndex = R.addIndex(R.map) - -module.exports = function(path, containerId, afterUpdateFunction) { - - // Stream of dragged elements - const draggedEls$ = flyd.stream() - - dragula([document.getElementById(containerId)]).on('dragend', draggedEls$) - - // Make a stream of objects with .id and .order - const giftOptions$ = flyd.map( getIdAndOrder , draggedEls$) - - function getIdAndOrder(el) { - var form = el.querySelector('input').form - var ids = serialize(form, {hash: true}).id - return {data: mapIndex((v, i) => ({id: v, order: i}), ids)} - } - - const updateOrdering = send => flyd.map(R.prop('body'), request({path, method: 'put' , send}).load) - - const response$ = flatMap(updateOrdering, giftOptions$) - - // Optional after update function - if(afterUpdateFunction) { - flyd.map(afterUpdateFunction, response$) - } -} - - diff --git a/client/js/components/duplicate_fundraiser.js b/client/js/components/duplicate_fundraiser.js deleted file mode 100644 index 5a847d23..00000000 --- a/client/js/components/duplicate_fundraiser.js +++ /dev/null @@ -1,26 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const request = require('../common/request') -const flatMap = require('flyd/module/flatmap') -const R = require('ramda') - -function init(prefix, fundraiserId) { - var dupePath = prefix + `/${fundraiserId}/duplicate.json` - var click$ = flyd.stream() - var button = document.getElementById('js-duplicateFundraiser') - - button.addEventListener('click', click$) - - const duplicate = () => { - button.setAttribute('disabled', 'disabled') - button.innerHTML = 'Copying...' - return flyd.map(R.prop('body'), request({path: dupePath, method: 'post'}).load) - } - - const response$ = flatMap(duplicate, click$) - - flyd.map(resp => window.location = prefix + `/${resp.id}`, response$) -} - -module.exports = init - diff --git a/client/js/components/encode-plain-email.js b/client/js/components/encode-plain-email.js deleted file mode 100644 index 9a6a782e..00000000 --- a/client/js/components/encode-plain-email.js +++ /dev/null @@ -1,20 +0,0 @@ -// License: LGPL-3.0-or-later -const b64 = require('./b64') - -module.exports = o => { - var header = [ - 'MIME-Version: 1.0' - , `From: ${o.from}` - , `Reply-To: ${o.from}` - , `To: ${o.to}` - , `Subject: ${o.subject}`] - - if(o.cc) header = header.concat(`Cc: ${o.cc.join(',')}`) - if(o.bcc) header = header.concat(`Bcc: ${o.bcc.join(',')}`) - - var email = header.concat(['Content-Type: text/plain', '', o.body]) - .join('\r\n').trim() - - return b64.encode(email) -} - diff --git a/client/js/components/field-with-error.js b/client/js/components/field-with-error.js deleted file mode 100644 index d730bd10..00000000 --- a/client/js/components/field-with-error.js +++ /dev/null @@ -1,14 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const R = require('ramda') -const validatedForm = require('flimflam/ui/validated-form') - -module.exports = R.curryN(2, (formState, field) => { - const key = R.path(['data','props','name'], field) - const validatedField = validatedForm.field(formState, field) - const err = formState.errors$()[key] - return h('div', { - attrs: {'data-ff-field': err ? 'invalid' : 'valid', 'data-ff-field-error': err || ''} - }, [ validatedField ]) -}) - diff --git a/client/js/components/fundraising/add_header_image.js b/client/js/components/fundraising/add_header_image.js deleted file mode 100644 index 68db23e7..00000000 --- a/client/js/components/fundraising/add_header_image.js +++ /dev/null @@ -1,6 +0,0 @@ -// License: LGPL-3.0-or-later -if(app.header_image_url) { - var cssString = "display: block; background-image: url(" + app.header_image_url + ")" - document.getElementById('js-fundraisingHeader').className ='fundraisingHeader--image container' - document.getElementById('js-fundraisingHeader-image').style.cssText = cssString -} diff --git a/client/js/components/maps/cc_map.js b/client/js/components/maps/cc_map.js deleted file mode 100644 index 2ae7ab66..00000000 --- a/client/js/components/maps/cc_map.js +++ /dev/null @@ -1,110 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/client') -var map_options = require('./default_options') -var cc_map = {} -var info_window = false -var map_data - -// the endpoint is the only required param -// see maps_controller for endpoint options -cc_map.init = function(endpoint, options_obj, query) { - endpoint = window.location.origin + '/maps/' + endpoint - request.get(endpoint) - .query(query) - .end(function(err, resp) { - map_data = resp.body.data - var has_map = document.getElementById('google_maps') - if (app.map_provider === 'google') { - if (!has_map) { - var script = document.createElement('script') - script.type = 'text/javascript' - script.id = 'google_maps' - let key = "" - if (app.map_provider_options && app.map_provider_options.key) { - key = `key=${app.map_provider_options.key}&` - } - script.src = `https://maps.googleapis.com/maps/api/js?${key}callback=draw_map` - document.body.appendChild(script) - set_extra_options(options_obj) - } else { - set_extra_options(options_obj) - draw_map() - } - } - else { - if (has_map) - { - has_map.innerText = "Sorry, no map provider is installed" - } - else - { - var map = document.getElementById('googleMap') - map.innerText = "Sorry, no map provider is installed" - } - } - }) -} - - -function set_extra_options(obj){ - if(!obj){ - return - } - if(obj.center && obj.center.lat) { - map_options.lat = obj.center.lat - map_options.lng = obj.center.lng - } - map_options.disableDefaultUI = obj.disable_ui ? true : false - map_options.zoom = obj.zoom ? obj.zoom : map_options.zoom - map_options.fit_all = obj.fit_all ? true : false -} - - -window.draw_map = function () { - map_options.center = new google.maps.LatLng(map_options.lat, map_options.lng) - map_options.mapTypeId = google.maps.MapTypeId.NORMAL - var map = new google.maps.Map(document.getElementById('googleMap'), map_options) - add_markers(map) -} - - -function add_markers(map){ - var markers = [] - appl.def('map_data_count', map_data.length) - map_data.forEach(function(data){ - if (!data.latitude) { - return - } - var coordinates = new google.maps.LatLng(data.latitude, data.longitude) - var marker = new google.maps.Marker({ - position: coordinates, - map: map, - draggable: false, - icon: 'https://raw.githubusercontent.com/CommitChange/public-resources/master/images/cc-map-marker-pick-22.png', - data: data - }) - - google.maps.event.addListener(marker, 'click', function() { - if (info_window) { - info_window.close() - } - info_window = new google.maps.InfoWindow({ content: this.data.name }) - info_window.open(map,this) - var map_data = this.data - if(map_data.total_raised) { - map_data.total_raised = utils.cents_to_dollars(map_data.total_raised) - } - appl.def('map_data', map_data) - }) - markers.push(marker) - }) - if(map_options.fit_all) { - var bounds = new google.maps.LatLngBounds(); - for(var i = 0; i < markers.length; i++) { - bounds.extend(markers[i].getPosition()); - } - map.fitBounds(bounds); - } -} - -module.exports = cc_map diff --git a/client/js/components/maps/default_options.js b/client/js/components/maps/default_options.js deleted file mode 100644 index 88c7a992..00000000 --- a/client/js/components/maps/default_options.js +++ /dev/null @@ -1,14 +0,0 @@ -// License: LGPL-3.0-or-later -var styles = require('./styles'); - -module.exports = { - zoom: 4, - lat: 38.8794, - lng : -97.3222, - styles: styles.discreet, - mapTypeControl: false, - scrollwheel: false, - scaleControl: false, - streetViewControl: false, - overviewMapControl: false -} \ No newline at end of file diff --git a/client/js/components/maps/npo_coordinates.js b/client/js/components/maps/npo_coordinates.js deleted file mode 100644 index f5ffe605..00000000 --- a/client/js/components/maps/npo_coordinates.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = function(){ - if(app.nonprofit.latitude) { - return { - lat: app.nonprofit.latitude, - lng: app.nonprofit.longitude, - } - } -} diff --git a/client/js/components/maps/styles.js b/client/js/components/maps/styles.js deleted file mode 100644 index 8fa18881..00000000 --- a/client/js/components/maps/styles.js +++ /dev/null @@ -1,269 +0,0 @@ -// License: LGPL-3.0-or-later -var Styles = {} - -// style credit: https://snazzymaps.com/style/1735/discreet -// Originally under CC0 1.0 - -Styles.discreet = [ - { - "featureType": "administrative", - "elementType": "all", - "stylers": [ - { - "visibility": "off" - } - ] - }, - { - "featureType": "administrative", - "elementType": "geometry.stroke", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "administrative", - "elementType": "labels", - "stylers": [ - { - "visibility": "on" - }, - { - "color": "#716464" - }, - { - "weight": "0.01" - } - ] - }, - { - "featureType": "administrative.country", - "elementType": "labels", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "landscape", - "elementType": "all", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "landscape.natural", - "elementType": "geometry", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "landscape.natural.landcover", - "elementType": "geometry", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "all", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "geometry.fill", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "geometry.stroke", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "labels.text", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "labels.text.fill", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi", - "elementType": "labels.text.stroke", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "poi.attraction", - "elementType": "geometry", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "road", - "elementType": "all", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "road.highway", - "elementType": "all", - "stylers": [ - { - "visibility": "off" - } - ] - }, - { - "featureType": "road.highway", - "elementType": "geometry", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "road.highway", - "elementType": "geometry.fill", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "road.highway", - "elementType": "geometry.stroke", - "stylers": [ - { - "visibility": "simplified" - }, - { - "color": "#a05519" - }, - { - "saturation": "-13" - } - ] - }, - { - "featureType": "road.local", - "elementType": "all", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "transit", - "elementType": "all", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "transit", - "elementType": "geometry", - "stylers": [ - { - "visibility": "simplified" - } - ] - }, - { - "featureType": "transit.station", - "elementType": "geometry", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - "featureType": "water", - "elementType": "all", - "stylers": [ - { - "visibility": "simplified" - }, - { - "color": "#84afa3" - }, - { - "lightness": 52 - } - ] - }, - { - "featureType": "water", - "elementType": "geometry", - "stylers": [ - { - "visibility": "on" - } - ] - }, - { - featureType: "poi.business", - elementType: "labels", - stylers: [ - { visibility: "off" } - ] - }, - { - "featureType": "water", - "elementType": "geometry.fill", - "stylers": [ - { - "visibility": "on" - } - ] - } -] - -module.exports = Styles diff --git a/client/js/components/modal.js b/client/js/components/modal.js deleted file mode 100644 index a24d9c33..00000000 --- a/client/js/components/modal.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const modal = require('flimflam/ui/modal') - -// convenience wrapper for setting modal sizes -// sizes can be 'small' or 'large' -module.exports = (obj, size='medium') => - h('div', {class: {[`modal-${size}`] : size}}, [ modal(obj)]) - diff --git a/client/js/components/nonprofit-branding.js b/client/js/components/nonprofit-branding.js deleted file mode 100644 index ab8c4e3f..00000000 --- a/client/js/components/nonprofit-branding.js +++ /dev/null @@ -1,5 +0,0 @@ -// License: LGPL-3.0-or-later -import nonprofitBranding from '../../../javascripts/src/lib/nonprofitBranding'; - -module.exports = nonprofitBranding(app.nonprofit.brand_color) - diff --git a/client/js/components/number-input.js b/client/js/components/number-input.js deleted file mode 100644 index da5ff94c..00000000 --- a/client/js/components/number-input.js +++ /dev/null @@ -1,17 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const classObject = require('../common/class-object') - -module.exports = (name, placeholder, value, classes) => { -return h('input.max-width-2', { - props: { - type: 'number' - , - , name - , placeholder - , value - } - , class: classObject(classes) - }) -} - diff --git a/client/js/components/progress-bar.js b/client/js/components/progress-bar.js deleted file mode 100644 index 952065b3..00000000 --- a/client/js/components/progress-bar.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') - -// A progress bar component -// Only a view function -// Simply pass in a state object, which should have: -// - hidden: Boolean (whether to display the bar) -// - percentage: Integer (percentage complete for the bar) -// - status: String (status message to display) -function view(state) { - if(state.hidden) return '' - return h('div.u-centered', [ - h('div.progressBar.u-marginY--10', [ - h('div.progressBar-fill--striped', {style: {width: state.percentage + '%'}}) - ]) - , h('p.status.u-marginTop--10', [ state.status ]) - ]) -} - -module.exports = view - diff --git a/client/js/components/public-activities.js b/client/js/components/public-activities.js deleted file mode 100644 index 46a2ce39..00000000 --- a/client/js/components/public-activities.js +++ /dev/null @@ -1,64 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const h = require('snabbdom/h') -const R = require('ramda') -const moment = require('moment') -const request = require('../common/request') - -// type can be 'campaign' or 'event' -const init = (type, path) => { - const resp$ = request({method: 'get', path}).load - const formattedResp$ = flyd.map(formatResp[type], resp$) - return {formattedResp$} -} - -const ago = date => moment(date).fromNow() -const formatRecurring = o => o.recurring - ? `made a recurring contribution of` - : `contributed` - -const formatCampaign = r => - R.map(o => { - return { - name: o.supporter_name - , action: formatRecurring(o) + ' ' + o.amount - , date: ago(o.date) - } - }, r.body) - -const formatEvent = r => - R.map(o => { - return { - name: o.supporter_name - , action: `got ${o.quantity} ticket${o.quantity > 1 ? 's' : ''}` - , date: ago(o.created_at) - } - }, r.body) - -const formatResp = { - campaign: formatCampaign -, event: formatEvent -} - -const activities = data => { - return h('tr', [ - h('td.u-padding--10.u-fontSize--13', [h('strong', data.name), data.action? h('div.u-marginTop--3', data.action) : '']) - , h('td.u-textAlign--right.u-fontSize--12.strong.u-paddingRight--10', [h('small', data.date)]) - ]) -} - -const view = state => { - if(app.hide_activities) return '' - const mixin = content => - h('section.pastelBox--grey', [h('header', 'Recent Activity'), content]) - if (!state.formattedResp$()) - return mixin(h('div.u-padding--15.u-centered.u-color--grey', 'Loading...')) - if (!state.formattedResp$().length) - return mixin(h('div.u-padding--15.u-centered.u-color--grey', 'None yet')) - return mixin(h('table.u-margin--0.table--striped', [ - h('tbody', R.map(activities, state.formattedResp$())) - ])) -} - -module.exports = {init, view} - diff --git a/client/js/components/radio-and-label-wrapper.js b/client/js/components/radio-and-label-wrapper.js deleted file mode 100644 index 1bcbb45f..00000000 --- a/client/js/components/radio-and-label-wrapper.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -var h = require("virtual-dom/h") - -// a constructor function for creating radio-label pairs -module.exports = function(id, name, customAttributes, content, stream){ - var customAttributes = customAttributes ? customAttributes : {} - return [ - h('input', {type: 'radio', name: name, id: id, attributes: customAttributes, onclick: stream}), - h('label', {attributes: {'for': id}}, content) - ] -} diff --git a/client/js/components/radios.js b/client/js/components/radios.js deleted file mode 100644 index 7d53c9d9..00000000 --- a/client/js/components/radios.js +++ /dev/null @@ -1,24 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') -const uuid = require('uuid') - -// example: -// radios('frequency', [ -// {label: 'Monthly', checked: true} -// , {label: 'Quarterly'} -// , {label: 'Yearly'} -// ]) - -const radios = name => label => { - if(typeof label === 'string') label = {label: label} - const id = uuid.v1() - return h('div', [ - h('input', {props: {type: 'radio', id, name: name, value: label.label, checked: label.checked}}) - , h('label', {attrs: {for: id}},[h('span.sub.pl-1.font-weight-1', label.label)]) - ]) -} - -module.exports = (name, labels) => - h('div.no-padding-last-child', R.map(radios(name), labels)) - diff --git a/client/js/components/render-activities.js b/client/js/components/render-activities.js deleted file mode 100644 index ecb265a6..00000000 --- a/client/js/components/render-activities.js +++ /dev/null @@ -1,18 +0,0 @@ -// License: LGPL-3.0-or-later -const snabbdom = require('snabbdom') -const render = require('ff-core/render') -const activities = require('./public-activities') - -module.exports = (type, path) => { - const init = _ => activities.init(type, path) - - const view = state => activities.view(state) - - const patch = snabbdom.init([ - require('snabbdom/modules/class') - , require('snabbdom/modules/props') - , require('snabbdom/modules/style') - ]) - render({state: init(), view, patch, container: document.querySelector('#js-activities')}) -} - diff --git a/client/js/components/saving_indicator.js b/client/js/components/saving_indicator.js deleted file mode 100644 index 8df1f142..00000000 --- a/client/js/components/saving_indicator.js +++ /dev/null @@ -1,19 +0,0 @@ -// License: LGPL-3.0-or-later -var h = require("virtual-dom/h") - -module.exports = function(savingState) { - return h('div.savingIndicator.pastelBox--yellow' - , {style: { - position: 'fixed' - , top: '0' - , left: '50%' - , width: '70px' - , marginLeft: '-35px' - , textAlign: 'center' - , zIndex: '999999' - , fontSize: '14px' - , padding: '4px' - , display: savingState.hide ? 'none' : 'block' - }} - , savingState.text) -} diff --git a/client/js/components/search-table.js b/client/js/components/search-table.js deleted file mode 100644 index 28c318fd..00000000 --- a/client/js/components/search-table.js +++ /dev/null @@ -1,43 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') -const search = require('./search') - -const map = R.addIndex(R.map) - -const table = (data=[], header, row) => - h('table.width-full', R.concat(header, map(row, data))) - -const showMore = state => { - if(!state.hasMoreResults$()) return '' - return h('div.py-3.border-top.border-color-grey', [ - h('button', { - attrs: {disabled: state.loading$()} - , on: {click: [ state.searchLessQuery$, { - page: state.searchLessQuery$().page + 1 - , search: '' - , page_length: state.pageLength - }]}}, 'Show more') - ]) -} - -const searchForm = (state, placeholder) => - h('div.clearfix.py-3', [ - h('form.right', {on: {submit: state.submitSearch$}}, [ - search(state.loading$(), placeholder) - ]) - ]) - -const view = (state, header, row, placeholder) => { - return h('div', [ - h('div', [searchForm(state, placeholder)]) - , state.data$().length && state.data$()[0] - ? table(state.data$(), header, row) - : h('div.py-2.color-grey', state.loading$() ? 'Loading...' : 'No results') - , showMore(state) - , state.loading$() ? h('div.loader') : '' - ]) -} - -module.exports = view - diff --git a/client/js/components/search.js b/client/js/components/search.js deleted file mode 100644 index cbdb33ef..00000000 --- a/client/js/components/search.js +++ /dev/null @@ -1,10 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const input = require('./text-input') - -module.exports = (loading, placeholder='Search') => - h('div.table', [ - h('div.middle-cell', [ input('', placeholder) ]) - , h('div.middle-cell.pl-1', [ h('button', {attrs: {disabled: loading}},'Search') ]) - ]) - diff --git a/client/js/components/select.js b/client/js/components/select.js deleted file mode 100644 index a1b49a78..00000000 --- a/client/js/components/select.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') - -// example: -// select({ -// name: 'contact' -// , options: ['email', 'SMS', 'phone'] -// , placeholder: 'How would you like to be contacted' -// , selected: 'email' -// }) - -const option = selected => o => - h('option', {props: {value: o, selected: selected && selected === o}}, o) - -module.exports = obj => - h('select', {props: {name: obj.name}} - , R.concat( - [h('option', {props: {disabled: 'true', selected: obj.selected === undefined}}, obj.placeholder || 'Select One')] - , R.map(option(obj.selected), obj.options) - ) - ) - diff --git a/client/js/components/sepa-form.es6 b/client/js/components/sepa-form.es6 deleted file mode 100644 index ceeabae8..00000000 --- a/client/js/components/sepa-form.es6 +++ /dev/null @@ -1,110 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const validatedForm = require('ff-core/validated-form') -const button = require('ff-core/button') -const flyd = require('flyd') -flyd.flatMap = require('flyd/module/flatmap') -flyd.filter = require('flyd/module/filter') -flyd.sampleOn = require('flyd/module/sampleon') -const scanMerge = require('flyd/module/scanmerge') -const IBAN = require('iban') - - -const request = require('../common/request') - -// Form validation constraints, validator functions, and error messages: -const constraints = { - name: {required: true} -, iban: {required: true, ibanFormat: /[a-zA-Z]{2}\d{14}/} -, bic: {required: true} -} - -const validators = { ibanFormat: IBAN.isValid } - -// You can pass in the .hideButton boolean if you want to control whether the submit button is shown/hidden -// Pass in a .card object, which can have default objects for the card form (name, number, cvc, exp_month, etc) -// Pass in .path to set the endpoint for saving the card -// Pass in .payload for default data to send to the server for every card save request (such as a request token) -const init = (state) => { - state = state || {} - - var messages = { - required: I18n.t('nonprofits.donate.payment.card.errors.field.presence') - , ibanFormat: I18n.t('nonprofits.donate.payment.card.errors.field.format') - } - - state.form = validatedForm.init({constraints, validators, messages}) - state.supp$ = flyd.sampleOn(state.form.validData$, state.supporter) - state.sepa$ = flyd.combine((supporter, sepaParams) => { - return {sepa_params: sepaParams(), supporter_id: supporter()} - }, [state.supp$, state.form.validData$]) - - const response$ = flyd.flatMap(saveTransferData, state.sepa$) - state.reponseOk$ = flyd.filter(response => !response.error, response$) - state.error$ = flyd.map(R.prop('error'), flyd.filter(response => response.error, state.reponseOk$)) - state.saved$ = flyd.filter(response => !response.error, state.reponseOk$) - - state.loading$ = scanMerge([ - [state.form.validSubmit$, R.always(true)] - , [state.error$, R.always(false)] - , [state.saved$, R.always(false)] - ], false) - - return state -} - -// Save transfer details to our own servers, and return a response stream -function saveTransferData(params){ - return flyd.map(R.prop('body'), request({ - method: 'post' - , path: '/sepa' - , send: params - }).load - ) -} - -// -- Virtual DOM - -const view = state => { - var field = validatedForm.field(state.form) - return validatedForm.form(state.form, h('form.sepaForm', [ - h('div.u-background--grey.group.u-padding--8', [ - nameInput(field) - , ibanInput(field) - , bicInput(field) - ]) - , h('div.u-centered', [ - button({ - error$: state.hideErrors ? flyd.stream() : state.error$ - , buttonText: I18n.t('nonprofits.donate.payment.card.submit') - , loadingText: ` ${I18n.t('nonprofits.donate.payment.card.loading')}` - , loading$: state.loading$ - }) - ]) - ]) - ) -} - -const nameInput = (field, name) => - h('fieldset', [ - field(h('input.u-margin--0', - { props: { name: 'name' , value: name || '', placeholder: I18n.t('nonprofits.donate.payment.sepa.name') } } - )) - ]) - -const ibanInput = field => - h('fieldset.col-12.u-margin--0', [ - field(h('input.u-margin--0', - {props: { type: 'text' , name: 'iban' , placeholder: I18n.t('nonprofits.donate.payment.sepa.iban') } } - )) - ]) - -const bicInput = field => - h('fieldset.col-right-0.u-margin--0', [ - field(h('input.u-margin--0.hidden', - { props: { name: 'bic' , type: 'hidden', value:'NOTPROVIDED', placeholder: I18n.t('nonprofits.donate.payment.sepa.bic') } } - )) - ]) - -module.exports = {view, init} diff --git a/client/js/components/set-state-from-value.js b/client/js/components/set-state-from-value.js deleted file mode 100644 index 6f3ed0ad..00000000 --- a/client/js/components/set-state-from-value.js +++ /dev/null @@ -1,18 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = function (state, ev){ - var target = ev.target - var names = target.name.split('.') - var value = target.type === 'checkbox' ? target.checked : target.value - var nestedState = state - - for(var i = 0, len = names.length - 1; i < len; ++i) { - if(nestedState[names[i]] === undefined) return state - nestedState = nestedState[names[i]] - } - - var lastKey = names[names.length - 1] - - nestedState[lastKey] = value - - return state -} diff --git a/client/js/components/show-more-button.es6 b/client/js/components/show-more-button.es6 deleted file mode 100644 index a6d37d58..00000000 --- a/client/js/components/show-more-button.es6 +++ /dev/null @@ -1,35 +0,0 @@ -// License: LGPL-3.0-or-later -/* -A 'Show More' button component, useful for placing at the bottom of a listing of ajax'd data. - -the showMore button's state uses the following properties: - -moreLoading: boolean (you might want general "loading" and a separate "moreLoading" states) -remaining: integer (count of how many results are not shown and still left. If 0, the show more button hides) -*/ - - -const view = require('vvvview') -const h = require("virtual-dom/h") -const flyd = require('flyd') - -var $ = { - nextPageClicks: flyd.stream() -} - -const root = (moreLoading, remaining) => { - var buttonContent = moreLoading - ? h('span', [h('i.fa.fa-spin.fa-spinner'), ' Loading... ']) - : h('span', ' Show More ') - - return h('div.moreResults.group', {style: {display: remaining ? 'block' : 'none'}}, [ - h('button.button--micro.details', {disabled: moreLoading, onclick: $.nextPageClicks}, [ - buttonContent, - ]), - ' ', - h('a.button--micro.details', {href: '#'}, 'Back to Top') - ]) -} - -module.exports = {root: root, $streams: $} - diff --git a/client/js/components/state-selector.js b/client/js/components/state-selector.js deleted file mode 100644 index eecc4775..00000000 --- a/client/js/components/state-selector.js +++ /dev/null @@ -1,25 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') - -const geo = require('../common/geography') -const stateCodes = geo.stateCodes - - -// Generate a drop -// -// options are -// { -// default: val // default value to be selected among the options -// , name: str // name attribute of the select -// } - -function view(options) { - var stateOptions = R.map( - s => h('option', {props: {value: s, selected: options.default === s}}, s) - , stateCodes - ) - return h('select', {props: {name: options.name }}, stateOptions) -} - -module.exports = view diff --git a/client/js/components/styles/branded-wizard.js b/client/js/components/styles/branded-wizard.js deleted file mode 100644 index c22312f2..00000000 --- a/client/js/components/styles/branded-wizard.js +++ /dev/null @@ -1,71 +0,0 @@ -// License: LGPL-3.0-or-later -const colors = require('../nonprofit-branding') -const gradient = require('../../common/css-gradient') - -const bg = color => `background-color: ${color} !important;` - - -module.exports = _ => -` -.badge { - display: inline-block; - min-width: 10px; - padding: 3px 7px; - font-size: 11px; - font-weight: bold; - color: #fff; - line-height: 1; - vertical-align: middle; - white-space: nowrap; - text-align: center; - background-color: #9c9c9c; - border-radius: 10px; -} -.badge:empty { - display: none; -} - -button .badge { - position: relative; - top: -1px; -} - -.wizard-steps div.is-selected, -.wizard-steps button.is-selected { - ${bg(colors.lighter)} -} -.wizard-steps .button.white { - color: #494949; -} -.wizard-steps a:not(.button--small), -.ff-wizard-index-label.ff-wizard-index-label--accessible, -.wizard-index-label.is-accessible { - color: ${colors.dark} !important; -} -.wizard-steps input.is-selected { - border-color: ${colors.light} !important; -} -.wizard-steps button:not(.white):not([disabled]) { - ${bg(colors.dark)} -} -.wizard-steps .highlight { - ${bg(colors.lightest)} -} -.wizard-steps label, -.wizard-steps th { - color: #636363; -} - -.wizard-steps input[type='radio']:checked + label:before { - ${bg(colors.base)} -} - -.wizard-steps input[type='checkbox'] + label:before { - color: ${colors.base} !important; -} - -.ff-wizard-index-label.ff-wizard-index-label--current, -.wizard-index-label.is-current { - ${gradient('left', '#fbfbfb', colors.light)} -} -` \ No newline at end of file diff --git a/client/js/components/styles/render-styles.js b/client/js/components/styles/render-styles.js deleted file mode 100644 index 393cb43f..00000000 --- a/client/js/components/styles/render-styles.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = _ => { - var styleTag = document.createElement('style') - return styles => { - styleTag.innerHTML = styles - document.querySelector('head').appendChild(styleTag) - } -} - diff --git a/client/js/components/supporter-address-form.es6 b/client/js/components/supporter-address-form.es6 deleted file mode 100644 index c30b849a..00000000 --- a/client/js/components/supporter-address-form.es6 +++ /dev/null @@ -1,88 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const button = require('ff-core/button') -const serializeForm = require('form-serialize') -flyd.scanMerge = require('flyd/module/scanmerge') -flyd.flatMap = require('flyd/module/flatmap') -flyd.filter = require('flyd/module/filter') -flyd.mergeAll = require('flyd/module/mergeall') -const request = require('../common/request') - -// pass in your existing supporter data to prefill the form -// pass in your url endpoint for supporter updates -// pass in some default data for your update payload -function init(state) { - state = state || {} - state = R.merge({ - submit$: flyd.stream() - , supporter$: flyd.stream(state.supporter || {}) - , error$: flyd.stream() - }, state) - - state.updated$ = flyd.map( - ev => { - ev.preventDefault() - return serializeForm(ev.target, {hash: true}) - } - , state.submit$ ) - - state.supporter$ = flyd.merge(state.updated$, flyd.stream(state.supporter)) - - state.response$ = flyd.flatMap( - supporter => flyd.map(R.prop('body'), request({ - method: 'put' - , path: state.path || `/nonprofits/${app.nonprofit_id}/supporters` - , send: R.merge({supporter}, state.payload || {}) - }).load) - , state.updated$ ) - - state.loading$ = flyd.mergeAll([ - flyd.map(R.always(true), state.submit$) - , flyd.map(R.always(false), state.response$) - ]) - - return state -} - -// things you need in state: -// - a supporter object with name, address, city, state_code, country, zip_code -// - the $submitAddressUpdate stream -function view(state) { - var supporter = state.supporter - return h('form', { on: {submit: state.submit$}}, [ - h('div.layout--three.u-marginBottom--10', [ - h('span', [ - h('label', 'Name') - , h('input', {props: {name: 'name', placeholder: 'name', value: supporter.name}}) - ]) - , h('span', [ - h('label', 'Street Address') - , h('input', {props: {name: 'address', placeholder: 'address', value: supporter.address}}) - ]) - , h('span', [ - h('label', 'City') - , h('input', {props: {name: 'city', placeholder: 'city', value: supporter.city}}) - ]) - , ]) - , h('div.layout--three.u-marginBottom--15', [ - h('span', [ - h('label', 'State/Region') - , h('input', {props: {name: 'state_code', placeholder: 'state/region', value: supporter.state_code}}) - ]) - , h('span', [ - h('label', 'Postal Code') - , h('input', {props: {name: 'zip_code', placeholder: 'postal code', value: supporter.zip_code}}) - ]) - , h('span', [ - h('label', 'Country') - , h('input', {props: {name: 'country', placeholder: 'country', value: supporter.country}}) - ]) - ]) - , h('input', {props: {type: 'hidden', name: 'id', value: supporter.id}}) - , button(R.pick(['loading$', 'error$'], state)) - ]) -} - -module.exports = {view, init} diff --git a/client/js/components/supporter-fields.js b/client/js/components/supporter-fields.js deleted file mode 100644 index 39c1d4c4..00000000 --- a/client/js/components/supporter-fields.js +++ /dev/null @@ -1,170 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const geography = require('../common/geography') -const addressAutocomplete = require('./address-autocomplete-fields') - -// This component is just the fields without any form wrapper or submit button, which allows you to handle those pieces outside of here. - -function init(state, params$) { -//window.param$ = params$; //debug, make it global - state = state || {} - state = R.merge({ - selectCountry$: flyd.stream() - , supporter: R.merge({ - email: app.user ? app.user.email : undefined - }, R.pick(['first_name', 'last_name', 'phone', 'address', 'city', 'state_code', 'zip_code'], app.profile || {}) ) - , required: {} - }, state) - state.addressAutocomplete = addressAutocomplete.init({data$: flyd.stream(state.supporter)}, params$) - state.notUSA$ = flyd.mergeAll([ - flyd.stream(!app.show_state_field) - , flyd.map(select => !geography.isUSA(select.value), state.selectCountry$) - ]) - return state -} - -// Into state, pass: -// - to_ship (whether to show a "shipping address" message) -// - disallow_anonymous (nonprofit table has the 'no_anon' column) -// - autocomplete_supporter_address (nonprofit table has a corresponding column) -// - anonymous (Boolean) -// - first name -// - last name -// - phone -// - address -// - city -// - state_code -// - zip_code -// - profile_id -// - required: { (which fields to make required -// - name -// - email -// - address (will make address + city + state_code + zip_code all required) -// } -function view(state) { - const emailTitle = I18n.t('nonprofits.donate.info.supporter.email') + `${state.required.email ? `${I18n.t('nonprofits.donate.info.supporter.email_required')}` : ''}` - return h('div.u-marginY--10', [ - h('input', { props: { type: 'hidden' , name: 'profile_id' , value: state.supporter.profile_id } }) - , h('input', { props: { type: 'hidden' , name: 'nonprofit_id' , value: state.supporter.nonprofit_id || app.nonprofit_id } }) - , h('fieldset', [ - h('input.u-marginBottom--0', { - props: { - type: 'email' - , title: emailTitle - , name: 'email' - , required: state.required.email - , value: state.supporter.email - , placeholder: emailTitle - } - }) - ]) - , h('section.group', [ - h('fieldset.u-marginBottom--0.u-floatL.col-right-4', [ - h('input', { - props: { - type: 'text' - , name: 'first_name' - , placeholder: I18n.t('nonprofits.donate.info.supporter.first_name') - , required: state.required.first_name - , title: I18n.t('nonprofits.donate.info.supporter.first_name') - , value: state.supporter.first_name - } - }) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-4', [ - h('input', { - props: { - type: 'text' - , name: 'last_name' - , placeholder: I18n.t('nonprofits.donate.info.supporter.last_name') - , required: state.required.last_name - , title: I18n.t('nonprofits.donate.info.supporter.last_name') - , value: state.supporter.last_name - } - }) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-4', [ - h('input', { - props: { - type: 'text' - , name: 'phone' - , placeholder: I18n.t('nonprofits.donate.info.supporter.phone') - , title: I18n.t('nonprofits.donate.info.supporter.phone') - , required: state.required.phone - , value: state.supporter.phone - } - }) - ]) - ]) - , addressAutocomplete.view(state.addressAutocomplete) - ]) -} - -function manualAddressFields(state) { - state.selectCountry$ = state.selectCountry$ || flyd.stream() - var stateOptions = R.prepend( - h('option', {props: {value: '', disabled: true, selected: true}}, I18n.t('nonprofits.donate.info.supporter.state')) - , R.map( - s => h('option', {props: {selected: state.supporter.state_code === s, value: s}}, s) - , geography.stateCodes ) - ) - var countryOptions = R.prepend( - h('option', {props: {value: '', disabled: true, selected: true}}, I18n.t('nonprofits.donate.info.supporter.country')) - , R.map( - c => h('option', {props: {value: c[0]}}, c[1]) - , app.countriesList ) -) - return h('section.group.pastelBox--grey.u-padding--5', [ - state.to_ship ? h('label.u-centered.u-marginBottom--5', I18n.t('nonprofits.donate.info.supporter.shipping_address')) : '' - , h('fieldset.col-8.u-fontSize--14', [ - h('input.u-marginBottom--0', { - props: { - title: 'Address' - , placeholder: I18n.t('nonprofits.donate.info.supporter.address') - , type: 'text' - , name: 'address' - , value: state.supporter.address - } - }) - ]) - , h('fieldset.col-right-4.u-fontSize--14', [ - h('input.u-marginBottom--0', { - props: { - name: 'city' - , type: 'text' - , placeholder: I18n.t('nonprofits.donate.info.supporter.city') - , title: 'City' - , value: state.supporter.city - } - }) - ]) - , state.notUSA$() - ? showRegionField() - : h('fieldset.u-marginBottom--0.u-floatL.col-4', [ - h('select.select.u-fontSize--14.u-marginBottom--0', {props: {name: 'state_code'}}, stateOptions) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-4.u-fontSize--14', [ - h('input.u-marginBottom--0', { - props: {type: 'text', title: 'Postal code', name: 'zip_code', placeholder: I18n.t('nonprofits.donate.info.supporter.postal_code'), value: state.supporter.zip_code} - }) - ]) - , h('fieldset.u-marginBottom--0.u-floatL.col-right-8', [ - h('select.select.u-fontSize--14.u-marginBottom--0', { - props: { name: 'country' } - , on: {change: ev => state.selectCountry$(ev.currentTarget)} - }, countryOptions ) - ]) - ]) -} - -function showRegionField() { - if(app.show_state_field) { - h('input.u-marginBottom--0.u-floatL.col-4', {props: {type: 'text', title: 'Region', name: 'region', placeholder: I18n.t('nonprofits.donate.info.supporter.region'), value: state.supporter.state_code}}) - } else { - return "" - } -} - -module.exports = {view, init} diff --git a/client/js/components/tables/filtering/apply_filter.js b/client/js/components/tables/filtering/apply_filter.js deleted file mode 100644 index 90b523aa..00000000 --- a/client/js/components/tables/filtering/apply_filter.js +++ /dev/null @@ -1,131 +0,0 @@ -// License: LGPL-3.0-or-later -var format = require('../../../common/format') - -module.exports = function(scope) { - - appl.def(scope + '.filter_count', 0) - - var readable_keys = { - total_raised_greater_than: 'total contributed', - total_raised_less_than: 'total contributed', - last_payment_before: 'last payment', - location: 'location', - after_date: 'date', - before_date: 'date', - sort_date: 'date', - year: 'year', - sort_name: 'name', - campaign_id: 'campaign', - event_id: 'event', - sort_towards: 'towards', - sort_contributed: 'total contributed', - sort_last_payment: 'last payment', - sort_type: 'type', - sort_amount: 'amount', - amount_less_than: 'amount less than', - amount_greater_than: 'amount greater than', - amount: 'amount' - , has_contributed_during: 'contributed after' - , has_not_contributed_during: 'not contributed after' - , donation_type: 'payment type' - , recurring: 'recurring donors' - , tags: 'tags' - , notes: 'notes' - , custom_fields: 'custom fields' - } - - appl.def('readable_filter_names', function() { - var arr = [] - var q = appl[scope].query - for(var key in q) { - var name = readable_keys[key] - if(name && q[key] && q[key].length) arr.push(name) - } - return utils.uniq(arr).map(function(s) { return "" + s + "" }).join(' ') - }) - - appl.def('clear_all_filters', function() { - for(var key in appl[scope].query) { - appl.def(scope + '.query.' + key, '') - } - appl[scope].index() - document.querySelector('.filterPanel').reset() - $('.sortArrows').attr('sort', 'none') - appl.def(scope + '.filter_count', 0) - }) - - appl.def('apply_input_filter', function(name, val) { - if(val && !appl[scope].query[name]) { - appl.incr(scope + '.filter_count') - } else if(!val && appl[scope].query[name]) { - appl.decr(scope + '.filter_count') - } - appl.def(scope + '.query.' + name, val) - re_fetch() - }) - - appl.def('apply_sort_filter', function(name) { - var old_val = appl[scope].query[name] - if(!old_val || old_val === '') { - appl.incr(scope + '.filter_count') - appl.def(scope + '.query.' + name, 'desc') - } else if(old_val === 'asc') { - appl.decr(scope + '.filter_count') - appl.def(scope + '.query.' + name, '') - } else { - appl.def(scope + '.query.' + name, 'asc') - } - re_fetch() - }) - - appl.def('apply_checkbox_filter', function(el) { - el = appl.prev_elem(el) - var prop = scope + ".query." + el.name - if(el.checked) { - appl.incr(scope + '.filter_count') - appl.def(prop, 'true') - } else { - appl.decr(scope + '.filter_count') - appl.def(prop, '') - } - re_fetch() - }) - - // Instead of having checkboxes mark a single property as true/false, we want - // to have many checked checkboxes with the same name attribute get their - // values aggregated into a single array under the single property name. - // Eg. for tag filtering, we want the checked checkboxes to construct a - // single array of tag names. - appl.def('apply_checkbox_array_aggregator', function(el) { - el = appl.prev_elem(el) - var prop = scope + '.query.' + el.name - var array = appl[scope]['query'][el.name] || [] - if(el.checked) { - appl.incr(scope + '.filter_count') - array.push(el.value) - } else { - appl.decr(scope + '.filter_count') - array.splice(array.indexOf(el.value), 1) // Remove the tag name from the array - } - appl.def(prop, array) - re_fetch() - }) - - appl.def('apply_radio_filter', function(el) { - el = appl.prev_elem(el) - if(el.checked) { - var prop = scope + ".query." + el.name - if(el.value && !appl[prop]) appl.incr(scope + '.filter_count') - if(!el.value) appl.decr(scope + '.filter_count') - appl.def(prop, el.value) - re_fetch() - } - }) - - - function re_fetch() { - appl.def(scope + '.query.page', 1) - appl[scope].index() - } - -} // module.exports diff --git a/client/js/components/tables/search.es6 b/client/js/components/tables/search.es6 deleted file mode 100644 index cff5ecdb..00000000 --- a/client/js/components/tables/search.es6 +++ /dev/null @@ -1,51 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('virtual-dom/h') -const thunk = require('vdom-thunk') -const formToObj = require('../../common/form-to-object') -const flyd = require('flyd') -const filterStream = require('flyd/module/filter') - -// Uses an immutable state object with 'page' and 'search' keys -// Will also use a 'loading' key for loading animations -// Optionally pass in a 'placeholder' key for the placeholder text - -// Searches are streams of objects like {page: 1, search: 'xxyy'} - -// Whenever they blank out the search field, immediately re-request -var $searchKeyups = flyd.stream() -var $clearOuts = flyd.map( - () => ({page: 1, search: ''}), - filterStream( - ev => !ev.target.value.length, // all blank values - $searchKeyups)) - -// Search form submissions -var $searchSubmits = flyd.stream() -flyd.on(ev => ev.preventDefault(), $searchSubmits) -var $searches = flyd.merge( - $clearOuts, - flyd.map( - ev => formToObj(ev.target), - $searchSubmits)) - - -const root = state => - h('form.table-meta-search', { - onsubmit: $searchSubmits - }, [ - h('input', {type: 'hidden', name: 'page', value: 1}), - h('input', { - type: 'text', - name: 'search', - placeholder: state.get('placeholder') || 'Search', - value: state.get('search'), - onkeyup: $searchKeyups, - }), - h('button.button--input', {type: 'submit', disabled: state.get('loading')}, [ - h('i.fa.fa-search', {style: {display: state.get('loading') ? 'none' : ''}}), - h('i.fa.fa-spin.fa-spinner', {style: {display: state.get('loading') ? '' : 'none'}}) - ]) - ]) - -module.exports = {root: root, $streams: {searches: $searches}} - diff --git a/client/js/components/text-input.js b/client/js/components/text-input.js deleted file mode 100644 index 75cf386d..00000000 --- a/client/js/components/text-input.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const classObject = require('../common/class-object') - -module.exports = (name, placeholder, value, classes) => { -return h('input.max-width-2', { - props: { - type: 'text' - , name - , placeholder - , value - } - , class: classObject(classes) - }) -} diff --git a/client/js/components/textarea.js b/client/js/components/textarea.js deleted file mode 100644 index cd00057c..00000000 --- a/client/js/components/textarea.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const classObject = require('../common/class-object') - -module.exports = (name, placeholder, value, classes) => { -return h('textarea.max-width-2', { - props: { - name - , placeholder - , value - } - , class: classObject(classes) - }) -} - diff --git a/client/js/components/todos.js b/client/js/components/todos.js deleted file mode 100644 index 57637d41..00000000 --- a/client/js/components/todos.js +++ /dev/null @@ -1,26 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = function(cb){ - var request = require('../common/client') - var url = '/nonprofits/' + app.nonprofit_id - - appl.def('todos.loading', true) - - // data returns booleans - request.get(url + appl.todos_action).end(function(err, resp) { - if(!resp.ok) return - var data = resp.body - - cb(data, url) - - appl.def('todos.loading', false) - appl.def('todos.percent_done', todos_percentage()) - }) - - function todos_percentage() { - var finished_todos = 0 - appl.todos.items.forEach(function(item){ - if(item.done) finished_todos += 1 - }) - return Math.floor(finished_todos / appl.todos.items.length * 100) - } -} diff --git a/client/js/components/top-nav.js b/client/js/components/top-nav.js deleted file mode 100644 index 3809abd5..00000000 --- a/client/js/components/top-nav.js +++ /dev/null @@ -1,10 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') - -module.exports = title => - h('div.bg-grey-2', [ - h('div.container.px-2.py-1.table.width-full', [ - h('h4.m-0.middle-cell.py-1', title) - ]) - ]) - diff --git a/client/js/components/wizard.js b/client/js/components/wizard.js deleted file mode 100644 index e87f1c67..00000000 --- a/client/js/components/wizard.js +++ /dev/null @@ -1,59 +0,0 @@ -// License: LGPL-3.0-or-later -// Functionality for a wizard UI (eg. our donate button) - -appl.def('wizard', { - - set_step: function(wiz_name, step_name, el) { - appl.push({name: step_name, el: appl.prev_elem(el)}, wiz_name + '.steps') - }, - - show_step: function(wiz_name, index) { - var steps = appl[wiz_name].steps - steps.forEach(function(step) { step.el.style.display = 'none' }) - appl[wiz_name].steps[index].el.style.display = 'table-cell' - }, - - init: function(wiz_name, node) { - appl.def(wiz_name + '.current_step', 0) - appl[wiz_name].steps[0].is_accessible = true - appl.trigger_update(wiz_name + '.steps') - this.show_step(wiz_name, 0) - appl.prev_elem(node).style.display = 'table' - return appl - }, - - reset: function(wiz_name) { - var wiz = appl[wiz_name] - wiz.steps = wiz.steps.map(function(step) { - $(step.el).find('form').each(function() { this.reset() }) - step.is_accessible = false - return step - }) - wiz.steps[0].is_accessible = true - appl.trigger_update(wiz_name + '.steps') - appl.def(wiz_name + '.current_step', 0) - appl.wizard.show_step(wiz_name, 0) - return appl - }, - - jump: function(wiz_name, index) { - var wiz = appl[wiz_name] - if(!wiz.steps[index].is_accessible) return - appl.def(wiz_name + '.current_step', index) - this.show_step(wiz_name, index) - return appl - }, - - advance: function(wiz_name) { - var wiz = appl[wiz_name] - if(wiz.current_step + 1 >= wiz.steps.length) - wiz.on_complete() - appl.incr(wiz_name + '.current_step') - wiz.steps[wiz.current_step].is_accessible = true - appl.trigger_update(wiz_name + '.steps') - appl.wizard.show_step(wiz_name, wiz.current_step) - return appl - } - -}) - diff --git a/client/js/donations/create.js b/client/js/donations/create.js deleted file mode 100644 index 6bdbce09..00000000 --- a/client/js/donations/create.js +++ /dev/null @@ -1,49 +0,0 @@ -// License: LGPL-3.0-or-later -// This defines a create_donation function that will create a Donation and -// Charge in our database and on Stripe given a Supporter that has a valid Card -// -// Use this with the cards/fields.html.erb partial -// -// Call it like: create_donation(card_obj, donation_obj) -// where card object is the full card data (name, number, expiry, etc) from the cards/fields partial -// and donation_obj is all the donation data (amount, type, etc) -// -// This function will create a Donation if donation.recurring is falsy -// It will create a RecurringDonation if donation.recurring is true - -var create_card = require('../cards/create') -var format_err = require('../common/format_response_error') -var format = require('../common/format') -var request = require('../common/super-agent-promise') - -module.exports = create_donation - - -function create_donation(donation) { - if(donation.recurring_donation) { - var path = '/nonprofits/' + app.nonprofit_id + '/recurring_donations' - } else { - var path = '/nonprofits/' + app.nonprofit_id + '/donations' - } - if(donation.dollars) { - donation.amount = format.dollarsToCents(donation.dollars) - delete donation.dollars - } - return request.post(path).set('Content-Type', 'application/json').send( donation).perform() - // Reset the card form ui - .then(function(resp) { - appl.def('card_form', {status: '', error: false}) - return resp.body - }) - // Display any errors - .catch(function(resp) { - appl.def('card_form', { - loading: false, - error: true, - status: format_err(resp), - progress_width: '0%' - }) - throw new Error(resp) - }) -} - diff --git a/client/js/donations/create_offline.js b/client/js/donations/create_offline.js deleted file mode 100644 index 292357c9..00000000 --- a/client/js/donations/create_offline.js +++ /dev/null @@ -1,24 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../common/super-agent-promise') -var format = require('../common/format') - -module.exports = create_offsite_donation - -function create_offsite_donation(data, ui) { - ui.start() - if(data.dollars) { - data.amount = format.dollarsToCents(data.dollars) - delete data.dollars - } - if(data.date) data.date = format.date.toStandard(data.date) - return request.post('/nonprofits/' + app.nonprofit_id + '/donations/create_offsite') - .send({donation: data}).perform() - .then(function(resp) { - ui.success(resp) - return resp - }) - .catch(function(resp) { - ui.fail(resp) - throw new Error(resp) - }) -} diff --git a/client/js/events/discounts/index.js b/client/js/events/discounts/index.js deleted file mode 100644 index 267931b5..00000000 --- a/client/js/events/discounts/index.js +++ /dev/null @@ -1,37 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/client') -var R = require('ramda') - -appl.def('discounts.url', '/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/event_discounts') - -appl.def('discounts.index', function(){ - request.get(appl.discounts.url).end(function(err, resp) { - appl.def('discounts.data', resp.body || []) - }) -}) - -appl.discounts.index() - -appl.def('discounts.apply', function(node){ - var code = appl.prev_elem(node).value - var codes = R.pluck('code', appl.discounts.data) - if (!R.contains(code, codes)) { - appl.def('ticket_wiz.discounted_total_amount', false) - return - } - var discount_obj = R.find(R.propEq('code', code), appl.discounts.data) - var discount_mult = Number(discount_obj.percent) / 100 - var ticket_price = appl.ticket_wiz.total_amount - var discounted_ticket_price = ticket_price - Math.round(ticket_price * discount_mult) - if(discounted_ticket_price === 0){ - appl.def('ticket_wiz.post_data.kind', 'free') - } - appl.notify('Discount successfully applied') - appl.def('ticket_wiz.discounted_total_amount', discounted_ticket_price) - appl.def('ticket_wiz.post_data.event_discount_id', discount_obj.id) -}) - -if(app.current_event_editor) { - require('./manage') -} - diff --git a/client/js/events/discounts/manage.js b/client/js/events/discounts/manage.js deleted file mode 100644 index 96ab988d..00000000 --- a/client/js/events/discounts/manage.js +++ /dev/null @@ -1,93 +0,0 @@ -// License: LGPL-3.0-or-later -var R = require('ramda') -var request = require('../../common/client') -var format = require('../../common/format') - -appl.def('discounts.create_or_update', function(form_obj, node){ - appl.def('discounts.loading', true) - if(!validate(form_obj)) { - appl.def('discounts.loading', false) - return - } - if(form_obj.id) { - update_discount(form_obj) - } else { - delete form_obj.id - create_discount(form_obj) - } -}) - - -appl.def('discounts.show_new', function(){ - appl.def('discounts.editing', {id: '', name: '', percent: '', code: ''}) - appl.open_modal('createOrEditDiscountsModal') -}) - - -appl.def('discounts.show_edit', function(i){ - appl.def('discounts.editing', appl.discounts.data[i]) - appl.open_modal('createOrEditDiscountsModal') -}) - - -function update_discount(form_obj){ - request.put(appl.discounts.url + '/' + form_obj.id, form_obj) - .end(function(err, resp){ - after_create_or_edit("Discount successfully edited") - }) -} - - -appl.def('discounts.delete', function(id){ - request.del(appl.discounts.url + '/' + id).end(function(err, resp) { - appl.notify('Discount successfully deleted') - appl.discounts.index() - }) -}) - - -function create_discount(form_obj){ - request.post(appl.discounts.url, form_obj) - .end(function(err, resp){ - after_create_or_edit("Discount successfully added") - }) -} - - -function after_create_or_edit(message){ - appl.discounts.index() - appl.notify(message) - appl.open_modal("manageDiscountsModal") - appl.def('discounts.loading', false) -} - - -function validate(form_obj){ - var blanks =['name', 'percent', 'code'] - var message = '' - blanks.map(function(a, i) { - if(!form_obj[a]) { message += format.capitalize(a) + ', '} - }) - if (message) { - appl.notify(message + " can't be blank") - return false - } - var percent = Number(form_obj.percent) - if (!Boolean(percent) || percent <= 0) { - appl.notify("Percentage must be a number larger than 0") - return false - } - if(percent > 100) { - appl.notify("Percentage can't be more than 100") - return false - } - var codes = R.pluck('code', R.reject(function(x){ return x['id'] === Number(form_obj.id)}, appl.discounts.data)) - var hasDupeCodes = R.contains(form_obj.code, codes) - - if (hasDupeCodes){ - appl.notify("That code is already being used for this event. Please type another code.") - return false - } - return form_obj -} - diff --git a/client/js/events/index/page.js b/client/js/events/index/page.js deleted file mode 100644 index 459d6b1f..00000000 --- a/client/js/events/index/page.js +++ /dev/null @@ -1,8 +0,0 @@ -// License: LGPL-3.0-or-later -const renderListings = require('../listings') -renderListings(`/nonprofits/${app.nonprofit_id}/events/listings`) - -if(app.current_user) { - require('../../events/new/wizard') -} - diff --git a/client/js/events/listing-item/index.js b/client/js/events/listing-item/index.js deleted file mode 100644 index c9832b22..00000000 --- a/client/js/events/listing-item/index.js +++ /dev/null @@ -1,64 +0,0 @@ -// License: LGPL-3.0-or-later -const format = require('../../common/format') -const h = require('snabbdom/h') -const moment = require('moment-timezone') - -const dateTime = (startTime, endTime) => { - const tz = ENV.nonprofitTimezone || 'America/Los_Angeles' - startTime = moment(startTime).tz(tz) - endTime = moment(endTime).tz(tz) - const sameDate = startTime.format("YYYY-MM-DD") === endTime.format("YYYY-MM-DD") - const ended = moment() > endTime ? ' (ended)' : '' - const format = 'MM/DD/YYYY h:mma' - const endTimeFormatted = sameDate ? endTime.format("h:mma") : endTime.format(format) - - return [ - h('strong', startTime.format(format) + ' - ' + endTimeFormatted) - , h('span.u-color--grey', ended) - ] -} - -const commaSeperate = arr => arr.filter(Boolean).join(', ') - -const metric = (label, val) => - h('span.u-inlineBlock.u-marginRight--20', [h('strong', `${label}: `), val || '0']) - -const row = (icon, content) => - h('tr', [ - h('td.u-centered', [h(`i.fa.${icon}`)]) - , h('td.u-padding--10', content) - ]) - -module.exports = e => { - const path = `/nonprofits/${app.nonprofit_id}/events/${e.id}` - const location = [ - h('p.strong.u-margin--0', e.venue_name) - , h('p.u-margin--0', commaSeperate([e.address, e.city, e.state_code, e.zip_code])) - ] - const attendeesMetrics = [ - metric('Attendees', e.total_attendees) - , metric('Checked In', e.checked_in_count) - , metric('Percent Checked In', Math.round((e.checked_in_count || 0) * 100 / (e.total_attendees || 0)) + '%') - ] - const moneyMetrics = [ - metric('Ticket Payments', '$' + format.centsToDollars(e.tickets_total_paid)) - , metric('Donations', '$' + format.centsToDollars(e.donations_total_paid)) - , metric('Total', '$' + format.centsToDollars(e.total_paid)) - ] - const links = [ - h('a.u-marginRight--20', {props: {href: path, target: '_blank'}}, 'Event Page') - , h('a', {props: {href: path + '/tickets', target: '_blank'}}, 'Attendees Page') - ] - return h('div.u-paddingTop--10.u-marginBottom--20', [ - h('h5.u-paddingX--20', e.name) - , h('table.table--striped.u-margin--0', [ - row('fa-clock-o', dateTime(e.start_datetime, e.end_datetime)) - , row('fa-map-marker', location) - , row('fa-users', attendeesMetrics) - , row('fa-dollar', moneyMetrics) - , row('fa-user', [h('strong', 'Organizer: '), e.organizer_email || 'None']) - , row('fa-link', links) - ]) - ]) -} - diff --git a/client/js/events/listings/index.js b/client/js/events/listings/index.js deleted file mode 100644 index eb942a36..00000000 --- a/client/js/events/listings/index.js +++ /dev/null @@ -1,57 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const render = require('ff-core/render') -const snabbdom = require('snabbdom') - -const request = require('../../common/request') -const listing = require('../listing-item') - -module.exports = pathPrefix => { - const get = param => { - const path = `${pathPrefix}?${param}=t` - return request({path, method: 'get'}).load - } - - const init = _ => { - return { - active: get('active') - , past: get('past') - , unpublished: get('unpublished') - , deleted: get('deleted') - } - } - - const listings = (key, state) => { - const resp$ = state[key] - const mixin = content => - h('section.u-marginBottom--30', [ - h('h5.u-centered.u-marginBottom--20', key.charAt(0).toUpperCase() + key.slice(1) + ' Events') - , h(`div.fundraiser--${key}`, content) - ]) - if(!resp$()) - return mixin([h('p.u-padding--15', 'Loading...')]) - if(!resp$().body.length) - return mixin([h('p.u-padding--15', `No ${key} events`)]) - return mixin(R.map(listing, resp$().body)) - } - - const view = state => - h('div', [ - listings('active', state) - , listings('past', state) - , listings('unpublished', state) - , listings('deleted', state) - ]) - - const container = document.querySelector('#js-eventsListing') - - const patch = snabbdom.init([ - require('snabbdom/modules/class') - , require('snabbdom/modules/props') - ]) - - render({ patch, container , view, state: init() }) -} - diff --git a/client/js/events/new/wizard.js b/client/js/events/new/wizard.js deleted file mode 100644 index d63fdedc..00000000 --- a/client/js/events/new/wizard.js +++ /dev/null @@ -1,59 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/pikaday-timepicker') -require('../../components/wizard') -require('../../common/image_uploader') -var checkName = require('../../common/ajax/check_campaign_or_event_name') -var format_err = require('../../common/format_response_error') - - -appl.def('advance_event_name_step', function(form_obj) { - var name = form_obj['event[name]'] - checkName(name, 'event', function(){ - appl.def('new_event', form_obj) - appl.wizard.advance('new_event_wiz') - }) -}) - -// Post a new event. -appl.def('create_event', function(el) { - var form_data = utils.toFormData(appl.prev_elem(el)) - form_data = utils.mergeFormData(form_data, appl.new_event) - appl.def('new_event_wiz.loading', true) - - post_event(form_data) - .then(function(req) { - appl.notify("Redirecting to your new event...") - appl.redirect(JSON.parse(req.response).url) - }) - .catch(function(req) { - appl.def('new_event_wiz.loading', false) - appl.def('new_event_wiz.error', req.responseText) - }) -}) - - -// Using the bare-bones XMLHttpRequest API so we can post form data and upload the image -function post_event(form_data) { - return new Promise(function(resolve, reject) { - var req = new XMLHttpRequest() - req.open("POST", '/nonprofits/' + app.nonprofit_id + '/events') - req.setRequestHeader('X-CSRF-Token', window._csrf) - req.send(form_data) - req.onload = function(ev) { - if(req.status === 200) resolve(req) - else reject(req) - } - }) -} - - -// Pikaday and timepicker initialization nonsense - -var Pikaday = require('pikaday') -var moment = require('moment') -new Pikaday({ - field: document.querySelector('#date-string-input'), - format: 'M/D/YYYY', - minDate: moment().toDate() -}) - diff --git a/client/js/events/show/editor.js b/client/js/events/show/editor.js deleted file mode 100644 index 0952b4fc..00000000 --- a/client/js/events/show/editor.js +++ /dev/null @@ -1,43 +0,0 @@ -// License: LGPL-3.0-or-later -// Functionality for Event Editors (a nonprofit admin, the event creator, or a super admin) - -require('../../common/image_uploader') - -const dupeIt = require('../../components/duplicate_fundraiser') - -var prefix = `/nonprofits/${app.nonprofit_id}/events` - -// takes prefix and fundraiser id -dupeIt(prefix, app.event_id) - -var url = `${prefix}/${app.event_id}` -var confirmation = require('../../common/confirmation') -var Pikaday = require('pikaday') -var moment = require('moment') - -require('../../components/ajax/toggle_soft_delete')(url, 'event') - -new Pikaday({ - field: document.querySelector('.date-picker'), - format: 'M/D/YYYY', - minDate: app.event_date || moment().toDate() -}) - -var editable = require('../../common/editable') - -editable($('#js-eventDescription'), { - sticky: true, - placeholder: "Add any event related text, images, videos or custom HTML here. We strongly recommend that this section is filled out with at least 250 words. It will be saved automatically as you type." -}) - -editable($('#js-customReceipt'), { - button: ["bold", "italic", "formatBlock", "align", "createLink", - "insertImage", "insertUnorderedList", "insertOrderedList", - "undo", "redo", "insert_donate_button", "html"] - , placeholder: "Add optional message here. It will be saved automatically as you type." -}) - - -appl.def('remove_this_image', function() { - appl.remove_background_image(url, 'event') -}) diff --git a/client/js/events/show/event_donation.js b/client/js/events/show/event_donation.js deleted file mode 100644 index 12cc64a5..00000000 --- a/client/js/events/show/event_donation.js +++ /dev/null @@ -1,27 +0,0 @@ -// License: LGPL-3.0-or-later -$('.ticket-level').click(function(e) { - wiz.model.set('single_amount', $(this).data('dollars')) - wiz.model.set('designation', $(this).data('name')) - wiz.model.set('description', $(this).data('desc')) - wiz.ticket_level_id = $(this).data('id') - wiz.donation.set({ - amount: $(this).data('amount'), - designation: $(this).data('name') - }) -}) - -$('.nonprofit-donate-button').click(function() { - wiz.model.set('single_amount', undefined) - wiz.model.set('designation', undefined) - wiz.model.set('description', undefined) - wiz.ticket_level_id = undefined - wiz.donation.set({ - amount: undefined, - designation: undefined - }) -}) - -$('.anon-wrapper').hide() -$('.info-submit').text('Submit') - -module.exports = wiz diff --git a/client/js/events/show/page.js b/client/js/events/show/page.js deleted file mode 100644 index 29c44ac7..00000000 --- a/client/js/events/show/page.js +++ /dev/null @@ -1,111 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/pikaday-timepicker') -require('../../common/fundraiser_metrics') -require('../../components/fundraising/add_header_image') -require('../../tickets/new') -require('../../ticket_levels/manage') -require('../discounts/index') -require('../../common/on-change-sanitize-slug') -const donateWiz = require('../../nonprofits/donate/wizard') -const snabbdom = require('snabbdom') -const h = require('snabbdom/h') -const flyd = require('flyd') -const R = require('ramda') -const render = require('ff-core/render') -const modal = require('ff-core/modal') -const noScroll = require('no-scroll') - -const on_ios11 = require('../../common/on-ios11') - -function createClickListener(startWiz$){ - return (...props) => { - if (on_ios11()) - { - noScroll.on() - } - startWiz$(...props) - } - - -} - -// -- Flim flam root component for event pages -function init() { - var state = { } - const startWiz$ = flyd.stream() - const donateButtons = document.querySelectorAll('.js-openDonationModal') - R.map(x => x.addEventListener('click', createClickListener(startWiz$)), donateButtons) - state.modalID$ = flyd.map(R.always('donationModal'), startWiz$) - flyd.on((id) => { - if (on_ios11() && id ===null ){ - noScroll.off() - }}, state.modalID$) - flyd.on((id) => { - if (on_ios11() && id !==null){ - noScroll.on() - }}, state.modalID$) - state.donateWiz = donateWiz.init(flyd.stream({event_id: app.event_id})) - return state -} - -function view(state) { - return h('div', [ - h('div.donationModal', [ - modal({ - thisID: 'donationModal' - , id$: state.modalID$ - , body: donateWiz.view(state.donateWiz) - }) - ]) - ]) -} - -// -- Render to page -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -render({state: init(), view, patch, container: document.querySelector('#js-main')}) - -const renderActivities = require('../../components/render-activities') - -if(!app.hide_activities) { - renderActivities('event', `/nonprofits/${app.nonprofit_id}/events/${app.event_id}/activities`) -} - -// -- Legacy viewscript stuff - - -if (app.nonprofit.brand_color) { - require('../../components/branded_fundraising') -} - -var request = require('../../common/client') -var path = '/nonprofits/' + app.nonprofit_id + '/events/' + app.event_id - - -if(app.current_event_editor) { - require('./editor') - require('./tour') - var create_info_card = require('../../supporters/info-card.es6') -} - - -// Event metrics init (total raised, total attendees) -appl.def('metrics.path_prefix', path + '/') -appl.ajax_metrics.index() - - -appl.ticket_wiz.on_complete = function(tickets) { - appl.ajax_metrics.index() -} - -appl.def('donate_wiz.donation.event_id', appl.event_id) - -appl.def('remove_event', function(e) { - request.del(path).end(function(err, resp) { - appl.redirect('/nonprofits/' + app.nonprofit_id + '/dashboard') - }) -}) diff --git a/client/js/events/show/tour.js b/client/js/events/show/tour.js deleted file mode 100644 index e510e598..00000000 --- a/client/js/events/show/tour.js +++ /dev/null @@ -1,35 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/vendor/bootstrap-tour-standalone') - -var tour_event = new Tour({ - steps: [ - { - orphan: true, - title: 'Welcome to your new event!', - content: "Hit 'Next' to find out how you can edit and add content to your event before sharing it." - }, - { - element: '.tour-admin', - placement: 'bottom', - title: 'Manage your event', - content: "You can manage your event by clicking on these buttons at the top of the page." - }, - { - element: '.froala-box', - placement: 'right', - title: 'Event details & story', - content: "You can add and format text and image content for your event by typing in this box. Adding images, video or even custom code can help enliven your event description. Click the icons at the top for formatting." - }, - { - orphan: true, - title: 'You’re on your way!', - content: "Once you've added content and ticket levels, and can start sharing your event." - } - ] -}) - - if($.cookie('tour_event') === String(app.nonprofit_id)) { - $.removeCookie('tour_event', {path: '/'}) - tour_event.restart() -} - diff --git a/client/js/events/stats/page.js b/client/js/events/stats/page.js deleted file mode 100644 index f9ca66d6..00000000 --- a/client/js/events/stats/page.js +++ /dev/null @@ -1,99 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const R = require('ramda') -const flyd = require('flyd') -const h = require('snabbdom/h') -const snabbdom = require('snabbdom') -const render = require('flimflam-render') -const filter = require('flyd/module/filter') -const flatMap = require('flyd/module/flatmap') -const every = require('flyd/module/every') - -const format = require('../../common/format') -const request = require('../../common/request') - -const eventsPath = `/nonprofits/${app.nonprofit_id}/events/${app.event_id}` - -const makeStatsSquare = vnode => { - const elm = vnode.elm - const height = elm.offsetHeight - const width = elm.offsetWidth - height > width - ? elm.style.width = height + 'px' - : elm.style.height = width + 'px' -} - -const get = path => R.compose( - flyd.map(x => x.body) - , filter(x => x.status === 200) - )(request({method: 'get', path}).load) - -// makes an ajax call on page load and then every minute -const getEveryMinute = path => flyd.merge( - flyd.stream({}) -, flatMap(time => get(path), every(60 * 1000))) - -const init = () => { - return { - metrics$: getEveryMinute(`${eventsPath}/metrics`) - , activities$: getEveryMinute(`${eventsPath}/activities`) - } -} - -const activity = a => - h('p.stats-activity' - , `${a.supporter_name} got ${a.quantity} ticket${a.quantity > 1 ? 's' : ''}`) - -const statInner = (content, isCircle) => { - const data = { - hook: {postpatch: makeStatsSquare} - , class: {'stat-inner--circular': isCircle} - } - return h('section.stat-inner' - , app.nonprofit.brand_color - ? R.merge(data, {style: {background: app.nonprofit.brand_color}}) - : data - , content) -} - -const view = state => -console.log('metrics', state.metrics$()) || - h('div', [ - h('section.stat-outer', [ - statInner([ - h('div.stat-text', [ - h('h3.stat-title', 'Raised') - , h('h3.stat-number', ['$', format.centsToDollars(state.metrics$().total_paid || 0)]) - ]) - ]) - ]) - , h('section.stat-outer', [ - statInner([ - h('div.stat-text', [ - h('h3.stat-title', 'Attendees') - , h('h3.stat-number', state.metrics$().total_attendees || '0') - ]) - ], true) - ]) - , !app.hide_activity_feed && state.activities$().length - ? h('div.stats-activities', R.map(activity, R.take(3, state.activities$()))) - : '' - , h('div.stats-backgroundScrim', '') - , app.event_background_image - ? h('div.stats-backgroundImage' - , {style: {'background-image': `url('${app.event_background_image}')`}}) - : '' - ]) - -const patch = snabbdom.init([ - require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -, require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/attributes') -]) - -const container = document.querySelector('#container') - -render({patch, container, view, state: init()}) - diff --git a/client/js/gift_options/admin.js b/client/js/gift_options/admin.js deleted file mode 100644 index 30ea8c4a..00000000 --- a/client/js/gift_options/admin.js +++ /dev/null @@ -1,101 +0,0 @@ -// License: LGPL-3.0-or-later -require('../common/restful_resource') -const reorder = require('../components/drag-to-reorder') -const format = require('../common/format') -const R = require('ramda') - -const url = `/nonprofits/${app.nonprofit_id}/campaigns/${app.campaign_id}/campaign_gift_options/update_order` - -reorder(url, 'js-reorderGifts', appl.ajax_gift_options.index) - -appl.def('ajax_gift_options', { - - update: function(form_obj, node) { - if(checkForAmount(form_obj)){ - return - } - var id = appl.gift_options.current.id - appl.ajax.update('gift_options', id, form_obj, node).then(function(resp) { - node.parentNode.reset() - appl.def('loading', false) - appl.ajax_gift_options.index() - appl.notify('Gift option updated successfully') - appl.open_modal('manageGiftOptionsModal') - }) - }, - - create: function(form_obj, node) { - if(checkForAmount(form_obj)){ - return - } - appl.ajax.create('gift_options', form_obj, node).then(function(resp) { - node.parentNode.reset() - appl.def('loading', false) - appl.open_modal('manageGiftOptionsModal') - appl.notify('Gift option created successfully') - appl.ajax_gift_options.index() - }) - }, - - del: function(id, node) { - var task = appl.ajax.del('gift_options', id, node) - task.then(function(resp) { - appl.open_modal('manageGiftOptionsModal') - appl.notify('Gift option removed successfully') - appl.ajax_gift_options.index() - }) - task.catch(function(resp){ - appl.open_modal('manageGiftOptionsModal') - appl.notify('This gift option has already been used. It can\'t be removed') - appl.ajax_gift_options.index() - }) - }, - -// Update or create a gift option depending on which mode we're in - save: function(form_obj, node) { - // the server expects both amount_one_time and amount_recurring to have - // a number value and this function passes in '0' as a fallback in the - // case that either input is left blank by the user - const toCents = x => format.dollarsToCents(x || '0') - - var data = R.evolve({ - amount_one_time: toCents - , amount_recurring: toCents - }, form_obj) - if(appl.gift_options.is_updating) { - appl.ajax_gift_options.update(data, node) - } else { - appl.ajax_gift_options.create(data, node) - } - }, -}) - - -function checkForAmount(form_obj) { - if(!form_obj.amount_one_time && !form_obj.amount_recurring) { - appl.notify('Please enter at least one amount') - return true - } else { - return false - } -} - -appl.def('gift_options', { - resource_name: 'campaign_gift_options', - path_prefix: '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id + '/', - - open_edit: function(gift_option) { - appl.def('gift_options', {current: gift_option, is_updating: true}) - .def('gift_option_action', 'Edit') - - appl.open_modal('giftOptionFormModal') - }, - - open_new: function() { - appl.def('gift_options', {current: undefined, is_updating: false}) - .def('gift_option_action', 'New') - document.querySelector("#giftOptionFormModal form").reset() - appl.open_modal('giftOptionFormModal') - } -}) - diff --git a/client/js/gift_options/index.js b/client/js/gift_options/index.js deleted file mode 100644 index 3d45bfe0..00000000 --- a/client/js/gift_options/index.js +++ /dev/null @@ -1,33 +0,0 @@ -// License: LGPL-3.0-or-later -require('../common/restful_resource') - -appl.def('gift_options', { - resource_name: 'campaign_gift_options', - path_prefix: '/nonprofits/' + app.nonprofit_id + '/campaigns/' + app.campaign_id + '/', -}) - -appl.def('ajax_gift_options.index', function() { - appl.ajax.index('gift_options').then(function(resp) { - var data = resp.body.data - appl.def('gift_options.data', supplementData(data)) - checkForQuantity(data) - }) -}) - -function supplementData(data) { - return data.map(function(x) { - if(x.quantity) { - var remaining = x.quantity - x.total_gifts - x.remaining = remaining > 0 ? remaining : 0 - } - return x - }) -} - -function checkForQuantity(data) { - data.forEach(function(x){ - if(x.quantity) { - appl.def('gift_options.has_any_quantities', true) - } - }) -} diff --git a/client/js/nonprofits/btn/page.js b/client/js/nonprofits/btn/page.js deleted file mode 100644 index 9a3505ac..00000000 --- a/client/js/nonprofits/btn/page.js +++ /dev/null @@ -1,19 +0,0 @@ -// License: LGPL-3.0-or-later - var Font = require('../../common/brand-fonts'), - utils = require('../../common/utilities'), - $brandedButton = $('.branded-donate-button') - - if(utils.get_param('fixed')){ - $brandedButton.addClass('is-fixed') - $('.centered').css('padding-top', '5px') - } - - var $logoBlue = '#42B3DF', - brandColor = app.nonprofit.brand_color || $logoBlue, - brandFont = Font[app.nonprofit.brand_font] || Font.bitter - - $brandedButton.css({ - 'background-color': brandColor, - 'font-family': brandFont - } - ) diff --git a/client/js/nonprofits/button/amounts.js b/client/js/nonprofits/button/amounts.js deleted file mode 100644 index 58bc2c0d..00000000 --- a/client/js/nonprofits/button/amounts.js +++ /dev/null @@ -1,60 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footer = require('./footer') -var radioAndLabelWrapper = require('../../components/radio-and-label-wrapper') - -var namePrefix = 'settings.amounts.' - -var nameStream = flyd.stream() - -module.exports = {root: root, stream: nameStream} - -function root(state) { - return [ - h('header.step-header', [h('h4.step-title', 'Amounts')]), - body(state) - ] -} - -function body(state){ - return h('div.step-inner', [ - menu(), - singleInput(state), - multipleInputs(state), - footer.root('Next', 'type') - ]) -} - -function menu() { - return h('section',[ - radioAndLabelWrapper('radio-multiple-amounts', namePrefix + 'name', {'checked': 'checked', 'value': 'multiple'}, - ["I want donors to be able to select from ", h('strong', 'multiple'), " amounts."], nameStream), - radioAndLabelWrapper('radio-single-amount', namePrefix + 'name', {'value': 'single'}, - ["I want a ", h('strong', 'single, preset'), " amount."], nameStream), - ]) -} - -function input(value, key) { - return h('span.prepend--dollar', - h('input.input--200', {name: namePrefix + key, value: value, onchange: nameStream}) - ) -} - -function displayIf(state, matcher) { - return state.settings.amounts.name === matcher ? 'block' : 'none' -} - -function singleInput(state) { - return h('div.u-marginTop--15', {style: {display: displayIf(state, 'single')}}, input(state.settings.amounts.single, 'single')) -} - -function multipleInputs(state) { - var multiples = state.settings.amounts.multiples - var inputs = [] - for (var key in multiples) { - inputs.push(input(multiples[key], 'multiples.' + key)) - } - return h('section.layout--three.u-marginTop--15', {style: {display: displayIf(state, 'multiple')}}, inputs) -} diff --git a/client/js/nonprofits/button/appearance.js b/client/js/nonprofits/button/appearance.js deleted file mode 100644 index 6a8742b5..00000000 --- a/client/js/nonprofits/button/appearance.js +++ /dev/null @@ -1,94 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footer = require('./footer') -var radioAndLabelWrapper = require('../../components/radio-and-label-wrapper') - -var appearanceStream = flyd.stream() - -module.exports = { - root: root, - stream: appearanceStream -} - -function root(state) { - return [ - h('header.step-header', [ - h('h4.step-title', 'Appearance'), - h('p', 'How would you like to accept donations?') - ]), - h('div.step-inner', [ - table(state), - customText(state), - footer.root('Next', 'designations') - ]) - ] -} - -function table(state) { - return h('table', [ - h('tr', [defaultButton(), fixedButton()]), - h('tr', [embeddedButton(), imageButton(state)]) - ]) -} - -function contentWrapper(title, content) { - return [title, h('div.u-paddingTop--15', content)] -} - -var color = app.nonprofit.brand_color ? app.nonprofit.brand_color : '#42B3DF' -var font = app.nonprofit.brand_font ? app.nonprofit.brand_font : 'inherit' -var buttonStyles = {background: color, 'font-family': font} - -var namePrefix = 'settings.appearance.' - -function defaultButton(){ - var title = 'Default button' - var content = [ h('p.branded-donate-button', {style: buttonStyles}, 'Donate'), - brandedButtonMessage()] - function brandedButtonMessage(){ - if(app.nonprofit.brand_color){return} - return h('p.u-paddingTop--15', - h('small', "To customize the color and font of your button, \ - head over to your settings page and click on 'branding'") - ) - } - return h('td', [radioAndLabelWrapper('radio-default', namePrefix + 'name', {'value': 'default', 'checked': 'checked'}, - contentWrapper(title, content), appearanceStream)]) -} - -function fixedButton(){ - var title = 'Fixed position button' - var content = [h('p.branded-donate-button.is-fixed', {style: buttonStyles}, 'Donate')] - return h('td', [radioAndLabelWrapper('radio-fixed', namePrefix + 'name', {'value': 'fixed'}, - contentWrapper(title, content), appearanceStream)]) -} - -function embeddedButton(){ - var title = 'Embed directly on page' - var content = [ h('img', {src: app.asset_path + "/graphics/mini-amount-step.png", title: title})] - return h('td', [radioAndLabelWrapper('radio-embedded', namePrefix + 'name', {'value': 'embedded'}, - contentWrapper(title, content), appearanceStream)]) -} - -function imageButton(state){ - var title = 'Custom image' - var defaultImg = app.asset_path + "/graphics/donate-elephant.png" - var imgUrl = state.settings.appearance.customImg ? state.settings.appearance.customImg : defaultImg - var content = [ h('img', {src: imgUrl, title: title}), - h('input', {type: 'text', name: namePrefix + 'customImg', placeholder: 'Add your image URL here', onkeyup: appearanceStream})] - return h('td', [radioAndLabelWrapper('radio-custom-image', namePrefix + 'name', {'value': 'custom image'}, - contentWrapper(title, content), appearanceStream)]) -} - -function customText(state) { - var text = state.settings.appearance.customText ? state.settings.appearance.customText : 'Donate' - var title = 'Custom text' - var content = [ - h('a.customText-text', text), - h('input', {type: 'text', name: namePrefix + 'customText', placeholder: 'Type here to change text', onkeyup: appearanceStream}) - ] - return h('section.customText-wrapper', [radioAndLabelWrapper('radio-custom-text', namePrefix + 'name', {'value': 'custom text'}, - contentWrapper(title, content), appearanceStream)]) -} diff --git a/client/js/nonprofits/button/designations.js b/client/js/nonprofits/button/designations.js deleted file mode 100644 index cc569d63..00000000 --- a/client/js/nonprofits/button/designations.js +++ /dev/null @@ -1,86 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footer = require('./footer') -var radioAndLabelWrapper = require('../../components/radio-and-label-wrapper') - -var nameStream = flyd.stream() -var countStream = flyd.stream() -var inputStream = flyd.stream() - - -flyd.map(function(keyup){ - keyup.target.value = keyup.target.value.replace(/[&"_*`'~]/g, "") -}, inputStream) - - -var namePrefix = 'settings.designations.' - -module.exports = { - root: root, - streams: { - name: flyd.merge(nameStream, inputStream), - count: countStream - } -} - -function root(state) { - return [ - h('header.step-header', h('h4.step-title', 'Designations')), - h('div.step-inner', - [ - body(state), - footer.root('Next', 'amounts') - ]) - ] -} - -function body(state){ - var desigs = state.settings.designations - return [menu(), - input(desigs), - inputs(desigs)] -} - -function menu(){ - return h('aside',[ - radioAndLabelWrapper('radio-no-designations', namePrefix + 'name', {'checked': 'checked', 'value': ''}, - ["I want ", h('strong', 'no'), " designation."], nameStream), - radioAndLabelWrapper('radio-single-designations', namePrefix + 'name', {'value': 'single'}, - ["I want a ", h('strong', 'single, preset'), " designation."], nameStream), - radioAndLabelWrapper('radio-multiple-designations', namePrefix + 'name', {'value': 'multiple'}, - ["I want donors to be able to select from ", h('strong', 'multiple'), " designations (up to 20)."], nameStream), - ]) -} - -function input(desigs){ - return h('input.u-marginTop--15.input--400', - {placeholder: 'Designation name', attributes: {'maxlength': 50}, name: namePrefix + 'single', style: {display: desigs.name === 'single' ? 'block' : 'none'}, - onchange: inputStream - } - ) -} - -function inputs(desigs){ - var prompt = [h('p.pastelBox--green.u-padding--10.u-marginY--10', 'If you would like to add a custom prompt to your donors, \ - please enter it below. Example: "Which radio show would you like to donate to?". The default prompt is "Please select a designation".'), - h('input.u-marginTop--10.input--400', - {placeholder: 'Prompt to donors', attributes: {'maxlength': 50}, name: namePrefix + 'prompt', onkeyup: inputStream}) - ] - var inputs = [] - for(var i = 0; i < desigs.count; i++) { - inputs.push(h('li', h('input.input--400', {attributes: {'maxlength': 50}, placeholder: 'Designation name', name: namePrefix + 'multiples.' + i, onchange: inputStream}))) - } - return h('div', {style: {display: desigs.name === 'multiple' ? 'block' : 'none'}}, [ - prompt, - h('p.pastelBox--blue.u-padding--10.u-marginY--10', 'Enter your designations below.'), - h('ol', [ - inputs, - h('a.button--tiny.edit', {onclick: countStream, attributes: isDisabled(desigs.count)}, [h('i.fa.fa-plus'), ' Add another designation']), - ]) - ]) -} - -function isDisabled(count){ if(count >= 20){return {'disabled' : ''}}} - diff --git a/client/js/nonprofits/button/footer.js b/client/js/nonprofits/button/footer.js deleted file mode 100644 index 0bb10d76..00000000 --- a/client/js/nonprofits/button/footer.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footerStream = flyd.stream() - -function root(text, next) { - return h('footer.step-footer', h('button.button', {data: {next: next}, onclick: footerStream}, text)) -} - -module.exports = {root: root, stream: footerStream} diff --git a/client/js/nonprofits/button/hide-dedication.js b/client/js/nonprofits/button/hide-dedication.js deleted file mode 100644 index 9541618c..00000000 --- a/client/js/nonprofits/button/hide-dedication.js +++ /dev/null @@ -1,31 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footer = require('./footer') - -var hideStream = flyd.stream() - -var name = 'hideDedication' - -module.exports = {root: root, stream: hideStream} - -function root(state) { - return [ - h('header.step-header', [h('h4.step-title', 'Hide dedication (optional)')]), - h('div.step-inner', [ - body(), - footer.root('Next', 'thankYou') - ]) - ] -} - -function body() { - var message = "If you don't want to give your donors the option to set a dedication, click the checkbox below." - - return [h('p.u-marginBottom--20', message), - h('input.u-marginTop--10', - {id: name + '-checkbox', type: 'checkbox', name: 'settings.' + name, onchange: hideStream}), - h('label.u-bold', {attributes: {for: name + '-checkbox'}}, 'Hide dedication') - ] -} diff --git a/client/js/nonprofits/button/page.js b/client/js/nonprofits/button/page.js deleted file mode 100644 index 4839cb41..00000000 --- a/client/js/nonprofits/button/page.js +++ /dev/null @@ -1,196 +0,0 @@ -// License: LGPL-3.0-or-later -var view = require("vvvview") -var flyd = require("flyd") -flyd.scanmerge = require("flyd/module/scanmerge") -var h = require("virtual-dom/h") - -var setStateFromValue = require('../../components/set-state-from-value') - -var appearance = require('./appearance') -var designations = require('./designations') -var amounts = require('./amounts') -var type = require('./type') -var hideDedication = require('./hide-dedication') -var thankYou = require('./thank-you') -var preview = require('./preview') - -var $footer = require('./footer').stream - -var state = { - page: window.location.hash.replace('#', '') - ? window.location.hash.replace('#', '') - : 'appearance', - settings: { - appearance: { - name: 'default', - customText: 'Donate' - }, - designations: {count: 1, multiples: {}}, - amounts: { - name: 'multiple', - single: 30, - multiples: {0: 10, 1: 20, 2: 30, 3: 70, 4: 100, 5: 200, 6: 1000 } - }, - type: { name: 'both'}, - thankYou: {} - } -} - -function root(state) { - return h('div', [ - menu(state), - pages(state) - ]) -} - -var $page = flyd.stream() - -var $pageClick = flyd.stream() - -flyd.map(function(ev){ - if(ev.target.data.page === 'preview') { - appendScript() - } else { - removeScript() - } -}, $pageClick) - -$page = flyd.merge($page, - flyd.map(function(ev) { - return ev.target.data.page - }, $pageClick)) - -function appendScript(){ - var script = document.createElement('script') - script.id = 'commitchange-donation-script' - script.setAttribute('data-npo-id', app.nonprofit_id) - script.setAttribute('src', app.host_with_port + '/js/donate-button.v2.js') - document.body.appendChild(script) -} - -function removeScript(){ - if(document.getElementById('commitchange-donation-script')){ - document.getElementById('commitchange-donation-script').remove() - } - removeButtonContent() -} - -function removeButtonContent(){ - var donateButton = document.querySelector('.commitchange-donate') - while(donateButton.lastChild){ - donateButton.removeChild(donateButton.lastChild) - } -} - -function appendButtonCode(){ - document.getElementById('choose-role-modal').classList.add('inView') - document.body.classList.add('is-showingModal') - var buttonWrapper = document.getElementById('js-donateButtonWrapper').cloneNode(true) - while(buttonWrapper.querySelector('iframe')) { - buttonWrapper.querySelector('iframe').remove() - } - while(buttonWrapper.querySelector('div')){ - buttonWrapper.querySelector('div').remove() - } - var code = buttonWrapper.innerHTML.replace(/"/g, "'") - document.getElementById('js-donateButtonAnchor').value = code - document.querySelector('#send-code-modal input[name="code"]').value = code -} - -function menu(state){ - var menuItems = [ - {name: 'appearance', text: 'Appearance'}, - {name: 'designations', text: 'Designations'}, - {name: 'amounts', text: 'Preset amounts'}, - {name: 'type', text: 'Preset recurring or one-time'}, - {name: 'hideDedication', text: 'Hide dedication' }, - {name: 'thankYou', text: 'Thank-you page'}, - {name: 'preview', text: 'Live preview'}] - - var lis =[] - var button = h('div.u-paddingX-10', - h('a.button--large.orange.u-width--full', {onclick: appendButtonCode}, 'Finish')) - - menuItems.map(function(item) { - var liClass = state.page === item.name ? '.active' : '' - lis.push(h('li' + liClass, {data: {page: item.name}, onclick: $pageClick}, item.text)) - }) - return h('aside.stepsMenu', [h('ul', lis), button]) -} - - - -function pageWrapper(state, pageName, content){ - return h('section.step.' + pageName, { - style: {display: state.page === pageName ? 'block' : 'none'} - }, content) -} - -function pages(state){ - return [ - pageWrapper(state, 'appearance', appearance.root(state)), - pageWrapper(state, 'designations', designations.root(state)), - pageWrapper(state, 'amounts', amounts.root(state)), - pageWrapper(state, 'type', type.root(state)), - pageWrapper(state, 'hideDedication', hideDedication.root(state)), - pageWrapper(state, 'thankYou', thankYou.root(state)), - pageWrapper(state, 'preview', preview.root(state)) - ] -} - -var donateFormBuilder = view(root, document.getElementById('js-donateFormBuilder'), state) - -var nameStreams = [appearance.stream, designations.streams.name, amounts.stream, type.stream, hideDedication.stream, thankYou.stream] - .map(function(stream) { return [stream, setStateFromValue]}) - -window.state = state - -var scanPairs = [ - [$page, setPage], - [$footer, advancePage], - [designations.streams.count, addDesignation] -].concat(nameStreams) - -var $state = flyd.immediate(flyd.scanmerge(scanPairs, state)) - -// rerenders the view based on state changes -// takes the view and state stream -flyd.map(donateFormBuilder, $state) - -function setPage(state, pageName){ - state.page = window.location.hash = pageName - return state -} - -function addDesignation(state, ev) { - if(state.settings.designations.count < 20) { - state.settings.designations.count++ - } - return state -} - -function advancePage(state, ev) { - state.page = ev.target.data.next - return state -} - - -// // Send email to webmaster -$('#send-code-modal form').on('submit', function(e) { - var self = this - e.preventDefault() - var data = $(this).serializeObject() - $(this).find('button').loading('Sending...') - $.post('/nonprofits/' + app.nonprofit_id + '/button/send_code.json', data) - .done(function() { - notification('Email sent!') - appl.close_modal() - }) - .complete(function() { - $(self).find('button').disableLoading() - }) - .fail(function(d) { - notification('Error: ' + utils.print_error(d)) - }) -}) - diff --git a/client/js/nonprofits/button/preview.js b/client/js/nonprofits/button/preview.js deleted file mode 100644 index 620c611d..00000000 --- a/client/js/nonprofits/button/preview.js +++ /dev/null @@ -1,159 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -module.exports = {root: root} - -function root(state) { - return [ - h('header.step-header', h('h4.step-title', 'Preview')), - h('div.step-inner', [ - body(state.settings) - ]) - ] -} - -function body(settings){ - if(settings.designations.name === 'multiple'){ - settings.designations.multiples = objToArray(settings.designations.multiples) - } - if(settings.amounts.name === 'multiple') { - settings.amounts.multiples = objToArray(settings.amounts.multiples) - } - return [ - h('p.strong.u-centered', 'Below is a live preview of your donate form'), - donateButton(settings), - table(settings) - ] -} - -function table(settings) { - var table = h('table.table--plaid',[ - h('tr', [h('td', 'Appearance'), appearanceTd(settings.appearance)]), - singleOrMultipleRow(settings.designations, 'Designation'), - singleOrMultipleRow(settings.amounts, 'Amount'), - h('tr', [h('td', 'Recurring or one-time'), h('td', settings.type.name)]), - h('tr', [h('td', 'Hide dedication'), h('td', ifAny(settings.hideDedication ? 'true' : h('span.u-color--grey', 'false')))]), - h('tr', [h('td', 'Thank-you page url'), h('td', ifAny(settings.thankYou.url))]), - ]) - return table -} - -function appearanceTd(data) { - if(data.name === 'custom image') { - return h('td', [data.name, h('p.u-color--grey', data.customImg)]) - } - if(data.name === 'custom text') { - return h('td', [data.name, h('p.u-color--grey', data.customText)]) - } - return h('td', data.name) -} - -function singleOrMultipleRow(obj, text) { - if(obj.name === 'single'){ - return h('tr', [h('td', text), h('td', obj.single+='')]) - } - if(obj.name === 'multiple'){ - return h('tr', [h('td', text + 's'), h('td', arrayToList(obj.multiples))]) - } - return h('tr', [h('td', text), h('td', h('span.u-color--grey', 'none'))]) -} - -function donateButton(settings) { - return h('div.u-centered.u-margin--20', {id: 'js-donateButtonWrapper'}, - h('a.commitchange-donate', {attributes: buttonAttributes(settings)}, - [buttonContent(settings.appearance)] - ) - ) -} - -function buttonAttributes(settings) { - var appearance = settings.appearance.name - var attrs = {} - if(appearance === 'custom image' || appearance === 'custom text') { - attrs['data-custom'] = '' - } - if (appearance === 'fixed') { - attrs['data-fixed'] = '' - } - if (appearance === 'embedded'){ - attrs['data-embedded'] = '' - } - if (settings.designations.name === 'single' && settings.designations.single) { - attrs['data-designation'] = settings.designations.single - } - if (settings.designations.name === 'multiple' && settings.designations.multiples.length) { - attrs['data-multiple-designations'] = arrayToStringWithSeparator(settings.designations.multiples, '_') - } - if (settings.designations.name === 'multiple' && settings.designations.prompt) { - attrs['data-designations-prompt'] = settings.designations.prompt - } - if (settings.amounts.name === 'single' && settings.amounts.single) { - attrs['data-amount'] = settings.amounts.single - } - if (settings.amounts.name === 'multiple' && settings.amounts.multiples.length) { - attrs['data-amounts'] = arrayToStringWithSeparator(settings.amounts.multiples, ',') - } - if (settings.thankYou.url) { - attrs['data-redirect'] = settings.thankYou.url - } - if (settings.type.name === 'one time') { - attrs['data-type'] = 'one-time' - } - if (settings.type.name === 'recurring') { - attrs['data-type'] = 'recurring' - } - if (settings.hideDedication) { - attrs['data-hide-dedication'] = '' - } - return attrs -} - - -function buttonContent(data) { - if (data.name === 'custom image') { - return h('img', {src: data.customImg}) - } - if (data.name === 'custom text') { - return h('span', data.customText) - } -} - -// todo: add to helpers or make global once we move away from view-script - -function arrayToStringWithSeparator(array, separator) { - return array.reduce(function(prev, current){ - return prev + separator + current - }) -} - -function camelCase(string) { - return string.split(" ").reduce(function(prev, current){ - return prev + current.charAt(0).toUpperCase() + current.slice(1) - }) -} - -function ifAny(data) { - if(data) { - return data - } - return h('span.u-color--grey', 'none') -} - -function objToArray(obj) { - var array = [] - for(var key in obj) { - if(obj[key]) { array.push(obj[key])} - } - return array -} - -function arrayToList(array , cssClass) { - var cssClass = cssClass ? cssClass : '.' + 'hasBullets--grey' - var lis = [] - array.map(function(item){ - item+='' - if(item && item.length) {lis.push(h('li', item))} - }) - return h('ul' + cssClass, lis) -} diff --git a/client/js/nonprofits/button/thank-you.js b/client/js/nonprofits/button/thank-you.js deleted file mode 100644 index 59d6c3b5..00000000 --- a/client/js/nonprofits/button/thank-you.js +++ /dev/null @@ -1,29 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") - -var footer = require('./footer') - -var namePrefix = 'settings.thankYou.' - -var urlStream = flyd.stream() - -module.exports = {root: root, stream: urlStream} - -function root(state) { - return [ - h('header.step-header', h('h4.step-title', 'Thank-you page (optional)')), - h('div.step-inner', [ - body(), - footer.root('Next', 'preview') - ]) - ] -} - -function body() { - var message = "You can provide a custom URL to your own thank-you page. Your donors will be directed to this page when they complete the donation. Be sure to include the 'http://' or 'https://' part of your url." - - return [h('p', message), - h('input.u-marginTop--10', {type: 'url', placeholder: 'Type your thank-you page URL here', name: namePrefix + 'url', onchange: urlStream}) - ] -} diff --git a/client/js/nonprofits/button/type.js b/client/js/nonprofits/button/type.js deleted file mode 100644 index fe734e3c..00000000 --- a/client/js/nonprofits/button/type.js +++ /dev/null @@ -1,41 +0,0 @@ -// License: LGPL-3.0-or-later -var flyd = require("flyd") -var h = require("virtual-dom/h") -var footer = require('./footer') -var radioAndLabelWrapper = require('../../components/radio-and-label-wrapper') - -var namePrefix = 'settings.type.' - -var nameStream = flyd.stream() - -module.exports = {root: root, stream: nameStream} - -function root() { - return [ - h('header.step-header', [h('h4.step-title', 'Recurring or One-Time')]), - body() - ] -} - -function body(){ - return h('div.step-inner', [ - menu(), - footer.root('Next', 'hideDedication') - ]) -} - -function menu() { - var recurringImg = h('img', {src: app.asset_path + "/graphics/recurring.svg"}) - var oneTimeImg = h('img', {src: app.asset_path + "/graphics/one-time.svg"}) - var message = "We highly recommend that you accept recurring donations whenever possible. They are a great source of recurring revenue!" - - return h('section',[ - h('p', message), - radioAndLabelWrapper('radio-type-both', namePrefix + 'name', {'checked': 'checked', 'value': 'both'}, - ["Recurring ", h('strong', 'and'), " one time.", recurringImg, oneTimeImg], nameStream), - radioAndLabelWrapper('radio-type-oneTime', namePrefix + 'name', {'value': 'one time'}, - [h('strong', 'Only '), " one time.", oneTimeImg], nameStream), - radioAndLabelWrapper('radio-type-recurring', namePrefix + 'name', {'value': 'recurring'}, - [h('strong', 'Only '), " recurring.", recurringImg], nameStream), - ]) -} diff --git a/client/js/nonprofits/cards/edit/index.es6 b/client/js/nonprofits/cards/edit/index.es6 deleted file mode 100644 index 7403cf75..00000000 --- a/client/js/nonprofits/cards/edit/index.es6 +++ /dev/null @@ -1,61 +0,0 @@ -// License: LGPL-3.0-or-later -const snabbdom = require('snabbdom') -const h = require('snabbdom/h') -const flyd = require('flyd') -const render = require('ff-core/render') -const notification = require('ff-core/notification') - -const format = require('../../../common/format') -const cardForm = require('../../../components/card-form.es6') - -function init() { - var state = { - card: pageLoadData.card - , plan: pageLoadData.plan - , subscription: pageLoadData.subscription - , daysLeft: pageLoadData.daysLeft - } - - state.cardForm = cardForm.init({ - name: app.profile.name - , zip_code: app.nonprofit.zip_code - , payload: { card: {holder_type: 'Nonprofit', holder_id: app.nonprofit_id, stripe_customer_id: pageLoadData.card.stripe_customer_id}} - , path: `/nonprofits/${app.nonprofit_id}/card` - }) - - // Notify on card update success - var message$ = flyd.map(()=>'Successfully updated! Now redirecting...', state.cardForm.saved$) - state.notification = notification.init({message$}) - - // For now, just redirect to settings page after updating card - flyd.map(resp => { window.location.href = '/settings' }, state.cardForm.saved$) - - return state -} - - -const view = state => - h('div.u-centered.u-maxWidth--600.u-margin--auto.u-marginTop--50.u-padding--15.js-view-confirm', [ - h('h4', `Payment Method for ${app.nonprofit.name}`) - , state.card.name ? h('p', `Current card: ${state.card.name}`) : '' - , h('p.u-strong', `Tier: ${state.plan.name} ($${format.centsToDollars(state.plan.amount)} ${state.plan.interval})`) - , h('hr') - , h('h5', 'Update Your Card:') - , h('div', [ cardForm.view(state.cardForm) ]) - - , h('br'), h('br'), h('br'), h('br') // lol - - , notification.view(state.notification) - ]) - - -// -- Render -var container = document.querySelector('#js-main') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -render({state: init(), view, container, patch}) - diff --git a/client/js/nonprofits/cards/edit/page.js b/client/js/nonprofits/cards/edit/page.js deleted file mode 100644 index f9b577f7..00000000 --- a/client/js/nonprofits/cards/edit/page.js +++ /dev/null @@ -1,4 +0,0 @@ -// License: LGPL-3.0-or-later - -require('./index.es6') - diff --git a/client/js/nonprofits/dashboard/page.js b/client/js/nonprofits/dashboard/page.js deleted file mode 100644 index 3a7d388d..00000000 --- a/client/js/nonprofits/dashboard/page.js +++ /dev/null @@ -1,83 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../campaigns/new/wizard') -require('../../events/new/wizard') -require('./tour') -appl.verify_identity = require('../payouts/index/verify_identity') -appl.create_bank_account = require('../../bank_accounts/create.es6') -var client = require('../../common/client') -var create_info_card = require('../../supporters/info-card.es6') -require('../payments_chart') - -appl.def('loading', true) - -client.get('/nonprofits/' + app.nonprofit_id + '/dashboard_metrics') - .end(function(err, resp) { - appl.def('loading', false) - appl.def('metrics.data', resp.body.data) - }) - -var map = require('../../components/maps/cc_map') -var npo_coords = require('../../components/maps/npo_coordinates')() -map.init('all-npo-supporters', {center: npo_coords}, {npo_id: app.nonprofit.id}) - -var todos = require('../../components/todos') -appl.def('todos_action', '/dashboard_todos') - -todos(function(data, url) { - appl.def('todos.items', [ - {text: "Collect first donation", done: data['has_donation'], link: url + '/button/basic' }, - {text: "Create first campaign", done: data['has_campaign'], modal_id: 'newCampaign', confirmed: true }, - {text: "Connect bank account", done: data['has_bank'], modal_id: 'newBankModal' }, - {text: "Create first event", done: data['has_event'], modal_id: 'newEvent', confirmed: true }, - {text: "Add a custom Thank You note for receipts", done: data['has_thank_you'], link: "/settings?p=receipts&s=settings-pane" }, - {text: "Import supporter data", done: data['has_imported'], link: url + '/supporters' }, - {text: "Brand fundraising tools", done: data['has_branding'], link: '/settings?p=branding&s=settings-pane' } - ]) - if(data['has_bank']){ - appl.todos.items.push({text: "Verify your identity", done: data['is_verified'], modal_id:'identityVerificationModal', confirmed: true}) - appl.def('todos.items', appl.todos.items) - } -}) - -// the only ff component so far on this page is events listings -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const render = require('ff-core/render') - -const request = require('../../common/request') -const listing = require('../../events/listing-item') - -const init = _ => { - var path = `/nonprofits/${app.nonprofit_id}/events/listings?active=t` - return {resp$: request({path, method: 'get'}).load} -} - -const view = state => { - const mixin = content => h('section', content) - if(!state.resp$()) - return mixin([h('p.u-padding--15.u-centered', 'Loading...')]) - if(!state.resp$().body.length) - return mixin([h('p.u-padding--15.u-centered', `None currently`)]) - return mixin(R.map(listing, state.resp$().body)) -} - -var container = document.querySelector('#js-eventsListing') - -const patch = require('snabbdom').init([ - require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/style') -, require('snabbdom/modules/attributes') -]) - -render({ patch, container , view, state: init() }) - - -// End-of-year report modal initialization and rendering -// XXX we should FLIMFLAMify the whole dashboard and make it a single tree with one render statement -const reportModal = require('../reports/modal') -const reportContainer = document.createElement('div') -document.body.appendChild(reportContainer) -render({state: reportModal.init(), view: reportModal.view, container: reportContainer, patch}) diff --git a/client/js/nonprofits/dashboard/tour.js b/client/js/nonprofits/dashboard/tour.js deleted file mode 100644 index 65e2f5b2..00000000 --- a/client/js/nonprofits/dashboard/tour.js +++ /dev/null @@ -1,68 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/vendor/bootstrap-tour-standalone') - -var $nav = $('.sideNav') -var $text = $('.sideNav-text') - -function showNav(){ - $nav.css('width', '240px') - $text.css({ - '-webkit-opacity' : '1', - '-moz-opacity': '1', - '-ms-opacity': '1', - 'opacity': '1' - }) -} - -function hideNav(){ - $nav.removeAttr('style') - $text.removeAttr('style') -} - -var dashboard_tour = new Tour({ - backdrop: false, - steps: [ - { - orphan: true, - title: 'Welcome to CommitChange!', - content: "This dashboard will give you a detailed overview of all of your fundraising activities. As you begin to raise money through donations, contributions and ticket sales, this dashboard will show more helpful information." - }, - { - element: '.tour-graph', - placement: 'bottom', - title: 'Graph', - content: "This graph will chart your donation history. You can change the time span from the top of the graph." - }, - { - element: '.tour-metrics', - placement: 'left', - title: 'Overview metrics', - content: "These metrics will help show you the big picture of your fundraising." - }, - { - element: '.tour-listings', - placement: 'left', - title: 'Recent metrics', - content: "These metrics will help give you a day-to-day picture of your fundraising. You can also create a new campaign or event by simply clicking on one of the orange buttons." - }, - { - backdrop: false, - orphan: true, - title: 'Navigation', - content: "To find other parts of our site, such as payments history, settings and your profile page, use the sidebar on the left.", - onHide: hideNav, - onShow: showNav, - }, - { - orphan: true, - title: "You're all set!", - content: "Check your inbox for an email confirmation link. We will verify your status as a nonprofit within 5-7 days. Contact support@commitchange.com if you have any questions. We're glad to have you on board!" - } - ] -}) - -if($.cookie('tour_dashboard') === String(app.nonprofit_id)) { - $.removeCookie('tour_dashboard', {path: '/'}) - dashboard_tour.init() - dashboard_tour.restart() -} diff --git a/client/js/nonprofits/donate/amount-step.js b/client/js/nonprofits/donate/amount-step.js deleted file mode 100644 index 26d0d2d9..00000000 --- a/client/js/nonprofits/donate/amount-step.js +++ /dev/null @@ -1,199 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const format = require('../../common/format') -flyd.scanMerge = require('flyd/module/scanmerge') - -function init(donationDefaults, params$) { - var state = { - params$: params$ - , evolveDonation$: flyd.stream() // Stream of objects that can be used to R.evolve the initial donation object - , buttonAmountSelected$: flyd.stream(true) // Whether the button or input is selected - , currentStep$: flyd.stream() - } - - // A stream of objects that an be used to modify the existing donation by using R.evolve - donationDefaults = R.merge(donationDefaults, { - amount: format.dollarsToCents(state.params$().single_amount || 0) - , designation: state.params$().designation - , recurring: state.params$().type === 'recurring' - , weekly: (typeof state.params$().weekly !== 'undefined') - }) - // Apply R.evolve using every value on the evolveDonation$ stream, starting with the defaults - state.donation$ = flyd.scanMerge([ - [state.params$ || flyd.stream(), setDonationFromParams] - , [state.evolveDonation$, R.flip(R.evolve)] - ], donationDefaults) - - return state -} - -const setDonationFromParams = (donation, params) => { - if(params.single_amount) { - donation.amount = format.dollarsToCents(params.single_amount) - } - else - donation.amount = undefined - if(params.designation) - donation.designation = params.designation - else - donation.designation = undefined - if (params.type === 'recurring') - donation.recurring = true - else - donation.recurring = undefined - return donation -} - -function view(state) { - const isRecurring = state.donation$().recurring - return h('div.wizard-step.amount-step', [ - chooseDesignation(state) - , recurringCheckbox(isRecurring, state) - , recurringMessage(isRecurring, state) - , amountFields(state) - , showSingleAmount(isRecurring, state) - ]) -} - - -// Dropdown to choose among custom designations -function chooseDesignation(state) { - if(!state.params$().multiple_designations) return '' - var defaultDesigs = [ - state.params$().designations_prompt || I18n.t('nonprofits.donate.amount.designation.choose') - , I18n.t('nonprofits.donate.amount.designation.most_needed') - ] - return h('section.u-paddingX--5', { - class: {'u-hide': !state.params$().multiple_designations} - }, [ - h('select.donate-designationDropdown.select.u-marginBottom--10', { - on: { change: ev => state.evolveDonation$({designation: R.always(ev.currentTarget.value)}) } - }, R.concat( - R.map( - d => h('option', {props: {value: ''}}, d) - , defaultDesigs - ) - , R.map( - d => h('option', {props: {value: d}}, d) - , state.params$().multiple_designations - ) - ) - ) - ]) -} - -// Checkbox to make the donation monthly recurring -function recurringCheckbox(isRecurring, state) { - if(state.params$().type === 'recurring' || state.params$().type === 'one-time') return '' - return h('section.donate-recurringCheckbox.u-paddingX--5 u-marginBottom--10', [ - h('div.u-padding--8.u-background--grey.u-centered', { - class: {highlight: isRecurring} - }, [ - h('input.u-margin--0.donationWizard-amount-input', { - props: {type: 'checkbox', selected: isRecurring, id: 'checkbox-recurring'} - , on: {change: ev => state.evolveDonation$({recurring: t => !t})} - }) - , h('label', {props: {htmlFor: 'checkbox-recurring'}}, composeTranslation( - I18n.t('nonprofits.donate.amount.sustaining') - , I18n.t('nonprofits.donate.amount.sustaining_bold') - ) - ) - ]) - ]) -} - -// If recurring, an extra message to reinforce that it is in fact charged every month -function recurringMessage(isRecurring, state) { - if(!isRecurring) return '' - var label=I18n.t('nonprofits.donate.amount.sustaining_selected') - var bolded=I18n.t('nonprofits.donate.amount.sustaining_selected_bold'); - if (state.donation$().weekly) { - label = label.replace(I18n.t('nonprofits.donate.amount.monthly'),I18n.t('nonprofits.donate.amount.weekly')); - bolded=I18n.t('nonprofits.donate.amount.weekly'); - } - return h('section.donate-recurringMessage.group', [ - h('p.u-paddingX--5.u-centered', { - class: {'u-hide': !isRecurring} - }, [ - state.params$().single_amount ? '' : h('small.info', composeTranslation(label,bolded)) - ]) - ]) -} - -function prependCurrencyClassname() { - if (app.currency_symbol === '$') { - return 'prepend--dollar' - } else if (app.currency_symbol === '€') { - return 'prepend--euro' - } -} - -function composeTranslation(full, bold) { - const texts = full.split(bold) - if(texts.length > 1) { - return [texts[0], h('strong', bold), texts[1]] - } else { - return full - } -} - -// All the buttons and the custom input for the amounts to select -function amountFields(state) { - if(state.params$().single_amount) return '' - return h('div.u-inline.fieldsetLayout--three--evenPadding', [ - h('span', - R.map( - amt => h('fieldset', [ - h('button.button.u-width--full.white.amount', { - class: {'is-selected': state.buttonAmountSelected$() && state.donation$().amount === amt*100} - , on: {click: ev => { - state.evolveDonation$({amount: R.always(format.dollarsToCents(amt))}) - state.buttonAmountSelected$(true) - state.currentStep$(1) // immediately advance steps when selecting an amount button - } } - }, [ - h('span.dollar', app.currency_symbol) - , String(amt) - ]) - ]) - , state.params$().custom_amounts || [] ) - ) - , h('fieldset.' + prependCurrencyClassname(), [ - h('input.amount.other', { - props: {name: 'amount', step: 'any', type: 'number', min: 1, placeholder: I18n.t('nonprofits.donate.amount.custom')} - , class: {'is-selected': !state.buttonAmountSelected$()} - , on: { - focus: ev => state.buttonAmountSelected$(false) - , change: ev => state.evolveDonation$({amount: R.always(format.dollarsToCents(ev.currentTarget.value))}) - } - }) - ]) - , h('fieldset', [ - h('button.button.u-width--full.btn-next', { - props: {type: 'submit', disabled: !state.donation$().amount || state.donation$().amount <= 0} - , on: {click: [state.currentStep$, 1]} - }, I18n.t('nonprofits.donate.amount.next')) - ]) - ]) -} - -// If the params have a single amount, show a large message saying how much it is -function showSingleAmount(isRecurring, state) { - if(!state.params$().single_amount) return '' - var gift = state.params$().gift_option || {} - if(state.params$().gift_option_name) gift.name = state.params$().gift_option_name - var desig = state.params$().designation - return h('section.u-centered', [ - h('p.singleAmount-message', [ - h('strong', app.currency_symbol + format.centsToDollars(format.dollarsToCents(state.params$().single_amount))) - , h('span.u-padding--0', { class: {'u-hide': !isRecurring} }, ' monthly') - , h('span', {class: {'u-hide': !state.params$().designation && !gift.id}}, [ ' for ' + (desig || gift.name) ]) - ]) - , h('button.button.u-marginBottom--20', {on: {click: [state.currentStep$, 1]}}, I18n.t('nonprofits.donate.amount.next')) - ]) -} - -module.exports = {view, init} - diff --git a/client/js/nonprofits/donate/dedication-form.js b/client/js/nonprofits/donate/dedication-form.js deleted file mode 100644 index 9771b513..00000000 --- a/client/js/nonprofits/donate/dedication-form.js +++ /dev/null @@ -1,97 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const uuid = require('uuid') - -// A contact info form for a donor to add a dedication in honor/memory of somebody - - -function view(state) { - var radioId1 = uuid.v1() // need unique ids for the checkbox id and label for attrs - var radioId2 = uuid.v1() - var data = state.dedicationData$() || {} - return h('form.dedication-form', { - on: {submit: ev => {ev.preventDefault(); state.submitDedication$(ev.currentTarget)}} - }, [ - h('p.u-centered.u-strong.u-marginBottom--10', I18n.t('nonprofits.donate.dedication.info')) - , h('fieldset.u-marginBottom--0.col-6', [ - h('input', {props: { - name: 'dedication_type' - , type: 'radio' - , id: radioId1 - , value: 'honor' - , checked: !data.dedication_type || data.dedication_type === 'honor' - }}) - , h('label', {props: {htmlFor: radioId1}}, I18n.t('nonprofits.donate.dedication.in_honor_label')) - ]) - , h('fieldset.u-marginBottom--0', [ - h('input', {props: { - name: 'dedication_type' - , type: 'radio' - , value: 'memory' - , id: radioId2 - , checked: data.dedication_type === 'memory' - }}) - , h('label', {props: {htmlFor: radioId2}}, I18n.t('nonprofits.donate.dedication.in_memory_label')) - ]) - , h('fieldset.u-marginBottom--0.col-6', [ - h('input', {props: { - name: 'first_name' - , placeholder: I18n.t('nonprofits.donate.dedication.first_name') - , title: 'First name' - , type: 'text' - , value: data.first_name - }}) - ]) - , h('fieldset.u-marginBottom--0', [ - h('input', {props: { - name: 'last_name' - , placeholder: I18n.t('nonprofits.donate.dedication.last_name') - , title: 'Last name' - , type: 'text' - , value: data.last_name - }}) - ]) - , h('fieldset.u-marginBottom--0.col-6', [ - h('input', {props: { - name: 'email' - , placeholder: I18n.t('nonprofits.donate.dedication.email') - , title: 'Email' - , type: 'text' - , value: data.email - }}) - ]) - , h('fieldset.u-marginBottom--0', [ - h('input', {props: { - name: 'phone' - , placeholder: I18n.t('nonprofits.donate.dedication.phone') - , title: 'Phone' - , type: 'text' - , value: data.phone - }}) - ]) - , h('fieldset.u-marginBottom--0', [ - h('input', {props: { - name: 'address' - , placeholder: I18n.t('nonprofits.donate.dedication.full_address') - , title: 'Address' - , type: 'text' - , value: data.address - }}) - ]) - , h('fieldset', [ - h('textarea', {props: { - name: 'dedication_note' - , placeholder: I18n.t('nonprofits.donate.dedication.note') - , title: 'Note' - , value: data.dedication_note - }}) - ]) - , h('div.u-centered', [ - h('button.button', I18n.t('nonprofits.donate.dedication.save')) - ]) - ]) -} - -module.exports = {view} diff --git a/client/js/nonprofits/donate/followup-step.js b/client/js/nonprofits/donate/followup-step.js deleted file mode 100644 index f1bd69d5..00000000 --- a/client/js/nonprofits/donate/followup-step.js +++ /dev/null @@ -1,39 +0,0 @@ -// License: LGPL-3.0-or-later - -const h = require('snabbdom/h') -function view(state) { - //if (window.parent) {window.parent.postMessage('commitchange:followup', '*');}; - - const supp = state.infoStep.savedSupp$() - return h('div.u-padding--10.u-centered', [ - h('h6.u-marginTop--15', I18n.t('nonprofits.donate.followup.success')) - , supp ? h('p', `${I18n.t('nonprofits.donate.followup.receipt_info')} ${supp.email}`) : '' - , h('hr') - , h('p', state.thankyou_msg || `${app.nonprofit.name} ${I18n.t('nonprofits.donate.followup.message')}`) - , h('div.u-inlineBlock.u-marginRight--10', [ - h('a.button--small.facebook.u-width--full.share-button', { - props: { - target: '_blank' - , href: 'https://www.facebook.com/dialog/feed?app_id='+app.facebook_app_id +"&display=popup&caption=" + encodeURIComponent(app.campaign.name || app.nonprofit.name) + "&link="+window.location.href - } - }, [h('i.fa.fa-facebook-square'), ` ${I18n.t('nonprofits.donate.followup.share.facebook')}`] ) - ]) - , h('div.u-inlineBlock.u-marginLeft--10.u-marginBottom--20', [ - h('a.button--small.twitter.u-width--full', { - props: { - target: '_blank' - , href: "https://twitter.com/intent/tweet?url="+window.location.href+"&via=CommitChange&text=Join me in supporting:" + (app.campaign.name || app.nonprofit.name) - } - }, [h('i.fa.fa-twitter-square'), ` ${I18n.t('nonprofits.donate.followup.share.twitter')}`] ) - ]) - // Show the 'finish' button only if we're in an offsite embedded modal - , state.params$().offsite - ? h('div', [ - h('button.button.finish', {on: {click: state.clickFinish$}}, I18n.t('nonprofits.donate.followup.finish')) - ]) - : '' - ]) -} - - -module.exports = {view} diff --git a/client/js/nonprofits/donate/get-params.js b/client/js/nonprofits/donate/get-params.js deleted file mode 100644 index f5920195..00000000 --- a/client/js/nonprofits/donate/get-params.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') - -const splitParam = str => - R.split(/[_;,]/, str) - -module.exports = params => { - const defaultAmts = '10,25,50,100,250,500,1000' - // Set defaults - const merge = R.merge({ - custom_amounts: '' - }) - // Preprocess data - const evolve = R.evolve({ - multiple_designations: splitParam - , custom_amounts: amts => R.compose(R.map(Number), splitParam)(amts || defaultAmts) - , custom_fields: fields => R.map(f => { - const [name, label] = R.map(R.trim, R.split(':', f)) - return {name, label: label ? label : name} - }, R.split(',', fields)) - }) - return R.compose(evolve, merge)(params) -} diff --git a/client/js/nonprofits/donate/info-step.js b/client/js/nonprofits/donate/info-step.js deleted file mode 100644 index f7c78189..00000000 --- a/client/js/nonprofits/donate/info-step.js +++ /dev/null @@ -1,187 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const uuid = require('uuid') -const supporterFields = require('../../components/supporter-fields') -const button = require('ff-core/button') -const dedicationForm = require('./dedication-form') -const serialize = require('form-serialize') -const request = require('../../common/request') -const format = require('../../common/format') - -const sepaTab = 'sepa' -const cardTab = 'credit_card' - -function init(donation$, parentState) { -//console.log(donation$().val); - var state = { - donation$: donation$ - , submitSupporter$: flyd.stream() - , submitDedication$: flyd.stream() - , params$: parentState.params$ - , currentStep$: flyd.stream() - , selectedPayment$: parentState.selectedPayment$ - } - - - // Save supporter for dedication logic - state.dedicationData$ = flyd.map(form => serialize(form, {hash: true}), state.submitDedication$) - const dedicationSuppData$ = flyd.map( - data => R.merge( - R.pick(['phone', 'email', 'address'], data) - , {name: `${data.first_name||''} ${data.last_name||''}`} - ) - , state.dedicationData$ - ) - state.showDedicationForm$ = flyd.map(()=> false, state.submitDedication$) - - // Save donor supporter record - state.supporterFields = supporterFields.init({required: {email: true}}, parentState.params$) - state.savedSupp$ = flyd.flatMap(postSupporter , flyd.map(formatFormData, state.submitSupporter$)) - state.savedDedicatee$ = flyd.map( - supporter => ({supporter, note: state.dedicationData$().dedication_note, type: state.dedicationData$().dedication_type}) - , flyd.flatMap(postSupporter, dedicationSuppData$) - ) - const changedDedication$ = flyd.merge(state.dedicationData$, state.savedDedicatee$) - state.supporter$ = flyd.merge(flyd.stream({}), state.savedSupp$) - - return state -} - -const formatFormData = form => { - const data = serialize(form, {hash: true}) - return R.evolve({customFields: R.toPairs}, data) -} - -const postSupporter = supporter => - flyd.map( - resp => resp.body - , request({ - method: 'post' - , path: `/nonprofits/${app.nonprofit_id}/supporters` - , send: R.merge(supporter, {locale: I18n.locale}) - }).load - ) - - -const customFields = fields => { - if(!fields) return '' - const input = field => h('input', { - props: { - name: `customFields[${field.name}]` - , placeholder: field.label - } - }) - return h('div', R.map(input, fields)) -} - -function recurringMessage(state){ -//function recurringMessage(isRecurring, state) { - var isRecurring=state.donation$().recurring; - var amountLabel = isRecurring ? ` ${I18n.t('nonprofits.donate.payment.monthly_recurring')}` : ` ${I18n.t('nonprofits.donate.payment.one_time')}` - var weekly= ""; - if (state.donation$().weekly) { - amountLabel = amountLabel.replace(I18n.t('nonprofits.donate.amount.monthly'),I18n.t('nonprofits.donate.amount.weekly')) + "*"; - weekly= h('div.u-centered.notice',[h("small",I18n.t('nonprofits.donate.amount.weekly_notice',{amount:(format.weeklyToMonthly(state.donation$().amount)/100.0),currency:app.currency_symbol}))]); - - } - return h('div', [ - h('p.u-fontSize--18 u.marginBottom--0.u-centered.amount', [ - h('span', app.currency_symbol + format.centsToDollars(state.donation$().amount)) - , h('strong', amountLabel) - ]) - , weekly] - ) -} - -function view(state) { - - var form = h('form', { - on: { - submit: ev => {ev.preventDefault(); state.currentStep$(2); state.submitSupporter$(ev.currentTarget)} - } - }, [ - recurringMessage(state) - , supporterFields.view(state.supporterFields) - , customFields(state.params$().custom_fields) - , dedicationLink(state) - , app.nonprofit.no_anon ? '' : anonField(state) - , h('fieldset.u-inlineBlock.u-marginTop--10', paymentMethodButtons(["card", "sepa"], state)) - ]) - return h('div.wizard-step.info-step.u-padding--10', [ - form - , h('div', { - style: {background: '#f8f8f8', position: 'absolute', 'top': '0', left: '3px', height: '100%', width: '99%'} - , class: {'u-hide': !state.showDedicationForm$(), opacity: 0, transition: 'opacity 1s', delay: {opacity: 1}} - }, [dedicationForm.view(state)] ) - ]) -} - -function paymentMethodButtons(paymentMethods, state){ - return h('section.group'), [ - paymentButton({error$: state.errors$, buttonText: I18n.t('nonprofits.donate.payment.tabs.sepa')}, sepaTab, state) - , paymentButton({error$: state.errors$, buttonText: I18n.t('nonprofits.donate.payment.tabs.card')}, cardTab, state) - ] -} - -function paymentButton(options, label, state){ - options.error$ = options.error$ || flyd.stream() - options.loading$ = options.loading$ || flyd.stream() - - let btnclass={ 'ff-button--loading': options.loading$() }; - btnclass[label]=true; - - return h('div.ff-buttonWrapper.u-floatL.u-marginBottom--10', { - class: { 'ff-buttonWrapper--hasError': options.error$() } - }, [ - h('p.ff-button-error', {style: {display: options.error$() ? 'block' : 'none'}} , options.error$()) - , h('button.ff-button', { - props: { type: 'submit', disabled: options.loading$() } - , on: { click: e => state.selectedPayment$(label) } - , class: btnclass - }, [ - options.loading$() ? (options.loadingText || " Saving...") : (options.buttonText || I18n.t('nonprofits.donate.payment.card.submit')) - ]) - ]) -} - -function anonField(state) { - state.anon_id = state.anon_id || uuid.v1() // we need a unique id in case there are multiple supporter forms on the page -- the label 'for' attribute needs to be unique - return h('div.u-marginTop--10.u-centered', [ - h('input', { - props: { - type: 'checkbox' - , name: 'anonymous' - , checked: state.anonymous - , id: `anon-checkbox-${state.anon_id}` - } - }) - , h('label', { - props: { - type: 'checkbox' - , htmlFor: `anon-checkbox-${state.anon_id}` - , id: 'anonLabel' - } - }, [ - h('small', I18n.t('nonprofits.donate.info.anonymous_checkbox')) - ]) - ]) -} - -const dedicationLink = state => { - if(state.params$().hide_dedication) return '' - return h('label.u-centered.u-marginTop--10', [ - h('small', [ - h('a', { - on: {click: [state.showDedicationForm$, true]} - }, state.dedicationData$() && state.dedicationData$().first_name - ? [h('i.fa.fa-check'), I18n.t('nonprofits.donate.info.dedication_saved') + `${state.dedicationData$().first_name || ''} ${state.dedicationData$().last_name || ''}`] - : [I18n.t('nonprofits.donate.info.dedication_link')] - ) - ]) - ]) -} - - -module.exports = {view, init} diff --git a/client/js/nonprofits/donate/page.js b/client/js/nonprofits/donate/page.js deleted file mode 100644 index 09fb6c87..00000000 --- a/client/js/nonprofits/donate/page.js +++ /dev/null @@ -1,85 +0,0 @@ -// License: LGPL-3.0-or-later -require('parsleyjs') - -const render = require('ff-core/render') -const donate = require('./wizard') -const snabbdom = require('snabbdom') -const flyd = require('flyd') -const R = require('ramda') -const url = require('url') - -const request = require('../../common/request') - -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) - -const params = url.parse(location.href, true).query -const params$ = flyd.stream(params) -app.params$ = params$ -if(params.campaign_id && params.gift_option_id) { - setGiftOptionParams(params.campaign_id, params.gift_option_id) -} - -// Listen to postMessages to change params -window.addEventListener('message', receiveMessage, false) -function receiveMessage(event) { - var ps - try { ps = JSON.parse(event.data) } - catch(e) {} - if(ps && ps.sender === 'commitchange') { - if (ps.command) { - var event = new CustomEvent('message:'+ps.command,{data:ps}); - container.dispatchEvent(event); - } - if(ps.command === 'setDonationParams') { - params$(ps) - // Fetch the gift option data if they passed a gift option id - if(ps.campaign_id && ps.gift_option_id) { - setGiftOptionParams(ps.campaign_id, ps.gift_option_id) - } - } - } -} - -// Given a gift option id, make a request to get its full data and set the other params accordingly -function setGiftOptionParams(campaign_id, gift_id) { - flyd.map( - resp => { - if(resp.status !== 200) return - var gift_option = resp.body.data - var params = params$() - params.gift_option = gift_option - params.single_amount = (gift_option.amount_one_time || gift_option.amount_recurring) / 100 - if(params.type === 'recurring' && gift_option.amount_recurring) { - params.single_amount = gift_option.amount_recurring / 100 - } else if(!gift_option.amount_one_time && gift_option.amount_recurring) { - params.type = 'recurring' - } else if(params.type === 'recurring' && !gift_option.amount_recurring) { - params.type = undefined - } - params$(params) - } - , request({ - method: 'get' - , path: `/nonprofits/${ENV.nonprofitID}/campaigns/${campaign_id}/campaign_gift_options/${gift_id}` - }).load - ) -} - -var state = donate.init(params$) -var container = document.querySelector('.js-donateForm') - -$(".donationWizard").trigger("render:pre"); -var event = new CustomEvent('render:pre'); -container.parentNode.dispatchEvent(event); -render({patch, view: donate.view, state, container}) -jQuery(function($){ -$(".donationWizard").trigger("render:post").addClass("displayed-updated"); -}); -// event = new CustomEvent('render:post'); -// container.parentNode.dispatchEvent(event); - diff --git a/client/js/nonprofits/donate/payment-step.js b/client/js/nonprofits/donate/payment-step.js deleted file mode 100644 index 524f5f33..00000000 --- a/client/js/nonprofits/donate/payment-step.js +++ /dev/null @@ -1,175 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -flyd.lift = require('flyd/module/lift') -flyd.flatMap = require('flyd/module/flatmap') -const request = require('../../common/request') -const cardForm = require('../../components/card-form.es6') -const sepaForm = require('../../components/sepa-form.es6') -const format = require('../../common/format') -const progressBar = require('../../components/progress-bar') - -const sepaTab = 'sepa' -const cardTab = 'credit_card' - -function init(state) { - const payload$ = flyd.map(supp => ({card: {holder_id: supp.id, holder_type: 'Supporter'}}), state.supporter$) - const supporterID$ = flyd.map(supp => supp.id, state.supporter$) - const card$ = flyd.merge( - flyd.stream({}) - , flyd.map(supp => ({name: supp.name, address_zip: supp.zip_code}), state.supporter$)) - - state.cardForm = cardForm.init({ path: '/cards', card$, payload$ }) - state.sepaForm = sepaForm.init({ supporter: supporterID$ } ) - - // Set the card ID into the donation object when it is saved - const cardToken$ = flyd.map(R.prop('token'), state.cardForm.saved$) - const donationWithCardToken$ = flyd.lift(R.assoc('token'), cardToken$, state.donation$) - - // Set the sepa transfer details ID into the donation object when it is saved - const sepaId$ = flyd.map(R.prop('id'), state.sepaForm.saved$) - const donationWithSepaId$ = flyd.lift(R.assoc('direct_debit_detail_id'), sepaId$, state.donation$) - - state.donationParams$ = flyd.immediate( - flyd.combine((sepaParams, cardParams, activeTab) => { - if(activeTab() == sepaTab) { - return sepaParams() - } else if(activeTab() == cardTab) { - return cardParams() - } - }, [donationWithSepaId$, donationWithCardToken$, state.activePaymentTab$]) - ) - const donationResp$ = flyd.flatMap(postDonation, state.donationParams$) - - state.error$ = flyd.mergeAll([ - flyd.map(R.prop('error'), flyd.filter(resp => resp.error, donationResp$)) - , flyd.map(R.always(undefined), state.cardForm.form.submit$) - , flyd.map(R.always(undefined), state.sepaForm.form.submit$) - , state.cardForm.error$ - , state.sepaForm.error$ - ]) - state.paid$ = flyd.filter(resp => !resp.error, donationResp$) - - // Control progress bar for card payment - state.progress$ = flyd.scanMerge([ - [state.cardForm.form.validSubmit$, R.always({status: I18n.t('nonprofits.donate.payment.loading.checking_card'), percentage: 20})] - , [state.cardForm.saved$, R.always({status: I18n.t('nonprofits.donate.payment.loading.sending_payment'), percentage: 100})] - , [state.cardForm.error$, R.always({hidden: true})] // Hide when an error shows up - , [flyd.filter(R.identity, state.error$), R.always({hidden: true})] // Hide when an error shows up - ], {hidden: true}) - - state.loading$ = flyd.mergeAll([ - flyd.map(R.always(true), state.cardForm.form.validSubmit$) - , flyd.map(R.always(true), state.sepaForm.form.validSubmit$) - , flyd.map(R.always(false), state.paid$) - , flyd.map(R.always(false), state.cardForm.error$) - , flyd.map(R.always(false), state.sepaForm.error$) - , flyd.map(R.always(false), state.error$) - ]) - - // Post the gift option, if necessary - const paramsWithGift$ = flyd.filter(params => params.gift_option_id || params.gift_option && params.gift_option.id, state.params$) - const paidWithGift$ = flyd.lift(R.pair, paramsWithGift$, state.paid$) - flyd.map( - R.apply((params, result) => postGiftOption(params.gift_option_id || params.gift_option.id, result)) - , paidWithGift$ - ) - - // post utm tracking details after donation is saved - flyd.map( - R.apply((utmParams, donationResponse) => postTracking(app.utmParams, donationResp$)) - , state.paid$ - ) - - return state -} - -const postGiftOption = (campaign_gift_option_id, result) => { - return flyd.map(R.prop('body'), request({ - path: '/campaign_gifts' - , method: 'post' - , send: {campaign_gift: {donation_id: result.json - ? result.json.donation.id // for recurring - : result.donation.id // for one-time - , campaign_gift_option_id}} - }).load) -} - -const postTracking = (utmParams, donationResponse) => { - const params = R.merge(utmParams, {donation_id: donationResponse().donation.id}) - - if(utmParams.utm_source || utmParams.utm_medium || utmParams.utm_content || utmParams.utm_campaign) { - return flyd.map(R.prop('body'), request({ - path: `/nonprofits/${app.nonprofit_id}/tracking` - , method: 'post' - , send: params - }).load) - } -} - -var posting = false // hack switch to prevent any kind of charge double post -// Post either a recurring or one-time donation -const postDonation = (donation) => { - if(posting) return flyd.stream() - else posting = true - var prefix = `/nonprofits/${app.nonprofit_id}/` - var postfix = donation.recurring ? 'recurring_donations' : 'donations' - - if(donation.weekly) { - donation.amount = Math.round(4.3 * donation.amount); - } - delete donation.weekly; // needs to be removed to be processed - - if(donation.recurring) donation = {recurring_donation: donation} - return flyd.map(R.prop('body'), request({ - path: prefix + postfix - , method: 'post' - , send: donation - }).load) -} - -const paymentTabs = (state) => { - if(state.activePaymentTab$() == sepaTab) { - return payWithSepaTab(state) - } else if(state.activePaymentTab$() == cardTab) { - return payWithCardTab(state) - } -} - -const payWithSepaTab = state => { - return h('div.u-marginBottom--10', [ - sepaForm.view(state.sepaForm) - ]) -} - -const payWithCardTab = state => { - return h('div.u-marginBottom--10', [ - cardForm.view(R.merge(state.cardForm, {error$: state.error$, hideButton: state.loading$()})) - , progressBar(state.progress$()) - ]) -} - -function view(state) { - var isRecurring = state.donation$().recurring - var dedic = state.dedicationData$() - var amountLabel = isRecurring ? ` ${I18n.t('nonprofits.donate.payment.monthly_recurring')}` : ` ${I18n.t('nonprofits.donate.payment.one_time')}` - var weekly=""; - if (state.donation$().weekly) { - amountLabel = amountLabel.replace(I18n.t('nonprofits.donate.amount.monthly'),I18n.t('nonprofits.donate.amount.weekly')) + "*"; - weekly= h('div.u-centered.notice',[h("small",I18n.t('nonprofits.donate.amount.weekly_notice',{amount:(format.weeklyToMonthly(state.donation$().amount)/100.0),currency:app.currency_symbol}))]); - } - return h('div.wizard-step.payment-step', [ - h('p.u-fontSize--18 u.marginBottom--0.u-centered.amount', [ - h('span', app.currency_symbol + format.centsToDollars(state.donation$().amount)) - , h('strong', amountLabel) - ]) - , weekly - , dedic && (dedic.first_name || dedic.last_name) - ? h('p.u-centered', `${dedic.dedication_type === 'memory' ? I18n.t('nonprofits.donate.dedication.in_memory_label') : I18n.t('nonprofits.donate.dedication.in_honor_label')} ` + `${dedic.first_name || ''} ${dedic.last_name || ''}`) - : '' - , paymentTabs(state) - ]) -} - -module.exports = {view, init} diff --git a/client/js/nonprofits/donate/plugins-available/alwaysAnonymous.js b/client/js/nonprofits/donate/plugins-available/alwaysAnonymous.js deleted file mode 100644 index 6a8cf68f..00000000 --- a/client/js/nonprofits/donate/plugins-available/alwaysAnonymous.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -jQuery(function($){ -$(".donationWizard").on("render:post", function(){ - var cb=document.getElementsByName("anonymous")[0]; - cb || console.log("ERROR: the checkbox anonymous ain't no more"); - cb.checked = true; - cb.parentNode.style.display="none"; - -}); - -}); diff --git a/client/js/nonprofits/donate/plugins-available/default-recurring.js b/client/js/nonprofits/donate/plugins-available/default-recurring.js deleted file mode 100644 index eb2816ae..00000000 --- a/client/js/nonprofits/donate/plugins-available/default-recurring.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -// This plugin allows to simplify the form (remove phone, address and city fields) if the url query has "minimal" -jQuery(function($){ - $(".donationWizard").on("render:post", function(){ - if (app.params$().default !== "recurring") return; - $("#checkbox-recurring").prop("checked",true); - document.getElementById("checkbox-recurring").dispatchEvent(new Event('change')); - }); -}) diff --git a/client/js/nonprofits/donate/plugins-available/dummy.js b/client/js/nonprofits/donate/plugins-available/dummy.js deleted file mode 100644 index 26b548e9..00000000 --- a/client/js/nonprofits/donate/plugins-available/dummy.js +++ /dev/null @@ -1,16 +0,0 @@ -// License: LGPL-3.0-or-later -(function () { - -var container = document.querySelector('.js-donateForm'); - -container.addEventListener('render:pre', function (e) { - console.log(e); - // e.target matches elem -}, false); - -container.addEventListener('render:post', function (e) { - console.log(e); - // e.target matches elem -}, false); - -})(); diff --git a/client/js/nonprofits/donate/plugins-available/ibanonly.js b/client/js/nonprofits/donate/plugins-available/ibanonly.js deleted file mode 100644 index b1fd3941..00000000 --- a/client/js/nonprofits/donate/plugins-available/ibanonly.js +++ /dev/null @@ -1,5 +0,0 @@ -// License: LGPL-3.0-or-later -jQuery(function($){ - $("input[name='bic']").closest("fieldset").hide(); - $("input[name='iban']").closest("fieldset").removeClass("col-8").addClass("col-12"); -}); diff --git a/client/js/nonprofits/donate/plugins-available/minamount.js b/client/js/nonprofits/donate/plugins-available/minamount.js deleted file mode 100644 index fed80060..00000000 --- a/client/js/nonprofits/donate/plugins-available/minamount.js +++ /dev/null @@ -1,4 +0,0 @@ -// License: LGPL-3.0-or-later -jQuery(function($){ - jQuery("input[name='amount']").attr("min",3); -}); diff --git a/client/js/nonprofits/donate/plugins-available/minimalForm.js b/client/js/nonprofits/donate/plugins-available/minimalForm.js deleted file mode 100644 index 1899e28b..00000000 --- a/client/js/nonprofits/donate/plugins-available/minimalForm.js +++ /dev/null @@ -1,20 +0,0 @@ -// License: LGPL-3.0-or-later -// This plugin allows to simplify the form (remove phone, address and city fields) if the url query has "minimal" -jQuery(function($){ -$(".donationWizard").on("render:post", function(){ - - if (!app.params$().minimal) return; - document.getElementsByName("phone")[0].style.display="none"; - document.getElementsByName("city")[0].style.display="none"; - document.getElementsByName("address")[0].style.display="none"; - - document.getElementsByName("first_name")[0].parentNode.classList.add('col-right-6'); - document.getElementsByName("first_name")[0].parentNode.classList.remove('col-right-4'); - document.getElementsByName("last_name")[0].parentNode.classList.add('col-right-6'); - document.getElementsByName("last_name")[0].parentNode.classList.remove('col-right-4'); - - document.getElementsByName("country")[0].parentNode.classList.add('col-right-8'); - document.getElementsByName("country")[0].parentNode.classList.remove('col-right-4'); - -}); -}); diff --git a/client/js/nonprofits/donate/plugins-available/piwik.js b/client/js/nonprofits/donate/plugins-available/piwik.js deleted file mode 100644 index cafad8d8..00000000 --- a/client/js/nonprofits/donate/plugins-available/piwik.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -var _paq = _paq || []; -/* tracker methods like "setCustomDimension" should be called before "trackPageView" */ -_paq.push(['trackPageView']); -_paq.push(['enableLinkTracking']); -var tracker=null; -(function() { -var u="//s.wemove.eu/"; -_paq.push(['setTrackerUrl', u+'piwik.php']); -_paq.push(['setSiteId', '5']); -var d=document, g=d.createElement('script'), s=d.getElementsByTagName('script')[0]; -g.onload = function() { - tracker=Piwik.getTracker("https://s.wemove.eu/piwik.php",5); - tracker.trackContentImpressionsWithinNode(document.getElementsByClassName("amount-step")[0]); -//trakcer.trackContentInteractionNode', document.getElementsByClassName("amount-step")[0], 'amount-step']); - tracker.trackContentImpressionsWithinNode(document.getElementsByClassName("info-step")[0]); - tracker.trackContentImpressionsWithinNode(document.getElementsByClassName("payment-step")[0]); - - tracker.trackContentImpressionsWithinNode(document.getElementsByClassName("ff-wizard-followup")[0]) -}; -g.type='text/javascript'; g.async=true; g.defer=true; g.src=u+'piwik.js'; s.parentNode.insertBefore(g,s); -//_paq.push(["setHeartBeatTimer",30]); -})(); diff --git a/client/js/nonprofits/donate/plugins-available/prefill-identity.js b/client/js/nonprofits/donate/plugins-available/prefill-identity.js deleted file mode 100644 index f972b391..00000000 --- a/client/js/nonprofits/donate/plugins-available/prefill-identity.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -// This plugin allows to automatically fill the form (name, address..) based on the url params - -jQuery(function($){ -$(".donationWizard").on("render:post", function(){ - ["email","first_name","last_name","city","zip_code","country"].forEach(function(k){ - var v=app.params$()[k]; - if (!v) return; - document.getElementsByName(k)[0].value=v; - }); - - var name =""; - if (app.params$().first_name) - name = app.params$().first_name + " "; - if (app.params$().last_name) - name += app.params$().last_name; - if (name.length > 1) { - document.getElementsByName("name").forEach(function(d){ - d.value=name; - }); - } -}); -}); diff --git a/client/js/nonprofits/donate/plugins-available/prettify.js b/client/js/nonprofits/donate/plugins-available/prettify.js deleted file mode 100644 index 2b757272..00000000 --- a/client/js/nonprofits/donate/plugins-available/prettify.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later -jQuery(function($){ - $(".closeButton").hide(); - if (app.currency_symbol != "€") $("button.sepa").hide(); - $("button.sepa").prepend('S€PA ').addClass("u-marginRight--10"); - $("button.credit_card").prepend(' '); - - $(".ff-wizard-followup a, .ff-wizard-followup button").hide(); // buttons FB and twitter bogus, finish too -}); diff --git a/client/js/nonprofits/donate/plugins-available/select-amount.js b/client/js/nonprofits/donate/plugins-available/select-amount.js deleted file mode 100644 index fdc02804..00000000 --- a/client/js/nonprofits/donate/plugins-available/select-amount.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -// This plugin allows to automatically chose an amount if the url query has "amount"=xz -jQuery(function($){ -$(".donationWizard").on("render:post", function(){ - - if (!app.params$().amount) return; - var amount= parseInt(app.params$().amount,10); - if (!amount > 0) return; - // TODO: check if the pre-selected amount is one of the buttons, instead of only putting it in "other" - $(".amount.other").val(amount).addClass("is-selected"); - $('.amount-step button.button').removeClass("is-selected"); - document.getElementsByName("amount")[0].dispatchEvent(new Event('change')); - $(".btn-next").click(); -}); -}); diff --git a/client/js/nonprofits/donate/wizard.js b/client/js/nonprofits/donate/wizard.js deleted file mode 100644 index 5993868f..00000000 --- a/client/js/nonprofits/donate/wizard.js +++ /dev/null @@ -1,208 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const R = require('ramda') -const h = require('snabbdom/h') -const url = require('url') -const render = require('ff-core/render') -const wizard = require('ff-core/wizard') -const scanMerge = require('flyd/module/scanmerge') -flyd.mergeAll = require('flyd/module/mergeall') -flyd.flatMap = require('flyd/module/flatmap') -flyd.zip = require('flyd-zip') - -const getParams = require('./get-params') - -const paymentStep = require('./payment-step') -const amountStep = require('./amount-step') -const infoStep = require('./info-step') -const followupStep = require('./followup-step') - -const request = require('../../common/request') -const format = require('../../common/format') - -const brandedWizard = require('../../components/styles/branded-wizard') -const renderStyles = require('../../components/styles/render-styles') - -renderStyles()(brandedWizard(null)) - -// pass in a stream of configuration parameters -const init = params$ => { - var state = { - error$: flyd.stream() - , loading$: flyd.stream() - , clickLogout$: flyd.stream() - , clickFinish$: flyd.stream() - , params$: flyd.map(getParams, params$) - } - - app.iframeParams = app.iframeParams || "" - app.utmParams = app.utmParams || {} - // maps utmParams from URL params string into object: - // { $utm_param: … } if params from iframe are present - app.iframeParams = app.iframeParams.split("?")[2] ? Object.assign(...app.iframeParams.split("?")[2].split("&").map((param) => param.split("=")).map( - array => ({[array[0]]: array[1]})) - ) : {} - - - app.utmParams = { - utm_campaign: app.utmParams.utm_campaign || app.iframeParams.utm_campaign, - utm_content: app.utmParams.utm_content || app.iframeParams.utm_content, - utm_medium: app.utmParams.utm_medium || app.iframeParams.utm_medium, - utm_source: app.utmParams.utm_source || app.iframeParams.utm_source - } - - app.campaign = app.campaign || {} // so we don't have to hot switch all the calls to app.campaign.name, etc - var donationDefaults = setDonationFromParams({ - nonprofit_id: app.nonprofit_id - , campaign_id: app.campaign.id - , event_id: app.event_id - }, state.params$()) - - state.selectedPayment$ = flyd.stream('sepa') - - state.amountStep = amountStep.init(donationDefaults, state.params$) - state.infoStep = infoStep.init(state.amountStep.donation$, state) - - state.donation$ = scanMerge([ - [state.amountStep.donation$, R.merge] - , [state.infoStep.savedSupp$, (d, supp) => R.assoc('supporter_id', supp.id, d)] - , [state.params$, setDonationFromParams] - , [state.infoStep.savedDedicatee$, setDonationDedication] - ], donationDefaults ) - - state.paymentStep = paymentStep.init({ - supporter$: state.infoStep.savedSupp$ - , donation$: state.donation$ - , dedicationData$: state.infoStep.dedicationData$ - , activePaymentTab$: state.selectedPayment$ - , params$: state.params$ - }) - - const currentStep$ = flyd.mergeAll([ - state.amountStep.currentStep$ - , state.infoStep.currentStep$ - , flyd.map(R.always(0), state.params$) // if the params ever change, jump back to step one - , flyd.stream(0) - ]) - state.wizard = wizard.init({currentStep$, isCompleted$: state.paymentStep.paid$}) - - // Save dedication as a supporter note once the donation is saved - // Requires the donor supporter, the dedicatee supporter, the dedication form data, and the paid donation - const dedicationParams$ = flyd.zip([state.infoStep.savedDedicatee$, state.infoStep.savedSupp$, state.paymentStep.paid$]) - const savedDedication$ = flyd.flatMap(R.apply(postDedication), dedicationParams$) - - // Log people out - flyd.map(ev => {request({method: 'get', path: '/users/sign_out'}); window.location.reload()}, state.clickLogout$) - - // Handle the Finish button from the followup step -- will close modal, redirect, or refresh - flyd.lift( - (ev, params) => { - if(!parent) return - if(params.redirect) parent.postMessage(`commitchange:redirect:${params.redirect}`, '*') - else if(params.mode !== 'embedded'){ - parent.postMessage('commitchange:close', '*'); - } else { - if (window.parent) {window.parent.postMessage('commitchange:close', '*');}; - } - } - , state.clickFinish$, state.params$ ) - - return state -} - -const setDonationFromParams = (don, params) => { - if(!params.single_amount || isNaN(format.dollarsToCents(params.single_amount))) delete params.single_amount - return R.merge({ - amount: params.single_amount ? format.dollarsToCents(params.single_amount) : 0 - , recurring: params.type === 'recurring' - , gift_option_id: params.gift_option_id - , designation: params.designation - }, don) -} - -// Set the text field to save to the server as serialized JSON -const setDonationDedication = (don, dedication) => { - return R.assoc( - 'dedication' - , JSON.stringify({ - supporter_id: dedication.supporter.id - , name: dedication.supporter.name - , contact: {email: dedication.supporter.email, - phone: dedication.supporter.phone, - address: dedication.supporter.address} - , note: dedication.note - , type: dedication.type - }) - , don) -} - - -// Save a dedication to the server by saving a note to the supporter -const postDedication = (dedication, donor, donation) => { - const pathPrefix = `/nonprofits/${ENV.nonprofitID}` - // TODO: translate content - var content = `[${donor.name}](${pathPrefix}/supporters?sid=${donor.id}) made a [donation of $${format.centsToDollars(donation.donation.amount)}](${pathPrefix}/payments?pid=${donation.payment.id}) in ${dedication.type || 'honor'} of this person.` - if(dedication.note) content += ` ${I18n.t('nonprofits.donate.dedication.donor_note')} "${dedication.note}".` - return flyd.map(r => r.body, request({ - method: 'post' - , path: `/nonprofits/${app.nonprofit_id}/supporters/${dedication.supporter.id}/supporter_notes` - , send: {supporter_note: {supporter_id: dedication.supporter.id, user_id: ENV.support_user_id, content}} - }).load) -} - -const view = state => { - return h('div.js-donateForm', { - class: {'is-modal': state.params$().offsite} - }, [ - h('img.closeButton', { - props: {src: '/assets/ui_components/close.svg'} - , on: {click: ev => state.params$().offsite && !state.params$().embedded ? parent.postMessage('commitchange:close', '*') : null} - , class: {'u-hide': !state.params$().offsite || !state.params$().embedded} - }) - , h('div.titleRow', [ - h('img', {props: {src: app.nonprofit.logo.normal.url}}) - , h('div.titleRow-info', [ - h('h2', app.campaign.name || app.nonprofit.name ) - , h('p', [ - state.params$().designation && !state.params$().single_amount - ? headerDesignation(state) - : app.campaign.tagline || app.nonprofit.tagline || '' - ]) - ]) - ]) - , wizardWrapper(state) - , h('footer.donateForm-footer', { - class: {'u-hide': !app.user} - }, [ - h('span', `${I18n.t('nonprofits.donate.signed_in')} `) - , h('strong', String(app.user && app.user.email)) - , h('a.logout-button', {on: {click: state.clickLogout$}}, ` ${I18n.t('nonprofits.donate.log_out')}`) - ]) - ]) -} - -const headerDesignation = state => { - return h('span', [ - h('i.fa.fa-star', {style: {color: app.nonprofit.brand_color || ''}}) - , h('strong', ` ${I18n.t('nonprofits.donate.amount.designation.label')} `) - , String(state.params$().designation) - , state.params$().designation_desc - ? h('span', [h('br'), h('small', state.params$().designation_desc)]) - : '' - ]) -} - -const wizardWrapper = state => { - return h('div.wizard-steps.donation-steps', [ - wizard.view(R.merge(state.wizard, { - steps: [ - {name: I18n.t('nonprofits.donate.amount.label'), body: amountStep.view(state.amountStep)} - , {name: I18n.t('nonprofits.donate.info.label'), body: infoStep.view(state.infoStep)} - , {name: I18n.t('nonprofits.donate.payment.label'), body: paymentStep.view(state.paymentStep)} - ] - , followup: followupStep.view(state) - })) - ]) -} - -module.exports = {view, init} diff --git a/client/js/nonprofits/edit/page.js b/client/js/nonprofits/edit/page.js deleted file mode 100644 index 1a69d7dc..00000000 --- a/client/js/nonprofits/edit/page.js +++ /dev/null @@ -1,8 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/image_uploader') -require('../../common/on-change-sanitize-slug') -var url = "/nonprofits/" + app.nonprofit_id - -appl.def('remove_this_image', function() { - appl.remove_background_image(url, 'nonprofit') -}) diff --git a/client/js/nonprofits/payments/index/page.js b/client/js/nonprofits/payments/index/page.js deleted file mode 100755 index 9db03097..00000000 --- a/client/js/nonprofits/payments/index/page.js +++ /dev/null @@ -1,102 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../../components/date_range_picker') -require('../../../common/panels_layout') -require('./tour') -require('../../../common/restful_resource') -require('../../../refunds/create') -require('../../supporters/get_name') -require('./payment_details') -require('../../../components/tables/filtering/apply_filter')('payments') -require('../../../common/ajax/get_campaign_and_event_names_and_ids')(app.nonprofit_id) -require('../../supporters/index/import') -var format = require('../../../common/format') - -appl.def('format', require('../../../common/format')) - -appl.def('payments.index', function() { - appl.def('loading', true) - appl.ajax.index('payments').then(function(resp) { - appl.def('loading', false) - if(appl.payments.query.page > 1) { - var main_panel = document.querySelector('.mainPanel') - main_panel.scrollTop = main_panel.scrollHeight - } - }) -}) - - -appl.def('payments.clear_search_if_deleted', function(val) { - if(val === '') { - appl.def('payments.query', {search: '', page: 1}) - appl.payments.index() - } -}) - - -appl.def("payments", { - query: {page: 1}, - concat_data: true -}) - -appl.def('filter_count', 0) - -if(window.location.search) - ajax_from_params() -else - appl.payments.index() - - -appl.def('payments.toggle_panel', function(id, el){ - var tr = el.parentNode - - if(tr.hasAttribute('data-selected')) { - appl.close_side_panel() - tr.removeAttribute('data-selected','') - } else { - appl.ajax_payment_details.fetch(id) - $('.mainPanel').find('tr').removeAttr('data-selected') - tr.setAttribute('data-selected','') - var path = window.location.pathname + "?pid=" + id - window.history.pushState({},'payment id', path) - } -}) - - -appl.def('readable_kind', function(kind, el) { - if(kind === "Donation") return "One-Time Donation" - else if(kind === "OffsitePayment") return "Offsite Donation" - else if(kind === "Ticket") return "Ticket Purchase" - else return format.camelToWords(kind) -}) - - -appl.def('kind_icon_class', function(kind) { - if(kind === "Donation") return "fa-heart" - if(kind === "OffsitePayment") return "fa-money" - if(kind === "RecurringDonation") return "fa-refresh" - if(kind === "Ticket") return "fa-ticket" - if(kind === "Refund") return "fa-rotate-left" -}) - -appl.def('formatted_gross_amount', function(amt) { - if(amt < 0) { - return '(' + appl.cents_to_dollars(Math.abs(amt)) + ')' - } else { - return appl.cents_to_dollars(amt) - } -}) - -function ajax_from_params() { - var payment_id = utils.get_param('pid') - var supporter_id = utils.get_param('sid') - appl.is_loading() - if(supporter_id) { - appl.payments.query = {page: appl.payments.query.page, search: supporter_id} - appl.payments.index() - } - if(payment_id) { - appl.payments.index() - appl.ajax_payment_details.fetch(payment_id) - } -} - diff --git a/client/js/nonprofits/payments/index/payment_details.js b/client/js/nonprofits/payments/index/payment_details.js deleted file mode 100644 index 5eb5e4d4..00000000 --- a/client/js/nonprofits/payments/index/payment_details.js +++ /dev/null @@ -1,166 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/super-agent-promise') -var readable_interval = require('../../recurring_donations/readable_interval') -var format = require('../../../common/format') - -appl.def('ajax_payment_details', { - fetch: function(id) { - appl.def('loading', true) - appl.def('payment_details.data', null) // since appl.def is a merge, we want to instead overwrite all data - appl.ajax.fetch('payment_details', id) - .then(function(resp) { - appl.def('loading', false) - appl.def('payment_details', appl.payment_details) - appl.def('payment_details.data.offsite_payment', appl.payment_details.data.offsite_payment) - appl.open_side_panel() - return appl.payment_details.data.charge && appl.payment_details.data.charge.id - }) - .then(fetch_refunds) - .catch(function(err) { - console.error(err) - appl.not_loading() - }) - } -}) - - -appl.def('payment_details', { - resource_name: 'payments' -}) - -// Utilities and view helper functions for payment details - -appl.def('payment_recurring_don', function(payment) { - return payment && payment.donation && payment.donation.recurring_donation -}) - -appl.def('get_recurring_interval', function(payment) { - var rd = appl.payment_recurring_don(payment) - if(!rd) return '' - return readable_interval(rd.interval, rd.time_unit) -}) - -appl.def('get_recurring_created', function(payment) { - var rd = appl.payment_recurring_don(payment) - if(!rd) return '' - return appl.readable_date(rd.created_at) -}) - -appl.def('payment_has_campaign', function(payment) { - return get_payment_campaign(payment) -}) - -appl.def('payment_campaign_name', function(payment) { - var c = get_payment_campaign(payment) - return c && c.name -}) - -appl.def('payment_campaign_url', function(payment) { - var c = get_payment_campaign(payment) - return c && c.url -}) - -appl.def('payment_has_event', function(payment) { - return payment && payment.event -}) - -appl.def('payment_event_name', function(payment) { - return payment && payment.event && payment.event.name -}) - -appl.def('payment_event_url', function(payment) { - return payment && payment.event && payment.event.url -}) - -// Given a payment, get either the designation, campaign name, or event name -appl.def('get_payment_purchase_object', function(payment) { - if(payment.tickets.length && payment.tickets[0].event) { - return "Event: " + payment.tickets[0].event.name - } else if(payment.donation) { - if(payment.donation.campaign) { - return "Campaign: " + payment.donation.campaign.name - } else { - if(payment.donation.designation) { - return "Designation: " + payment.donation.designation - } else if(payment.donation.dedication) { - return "In honor: " + payment.donation.dedication - } - } - } -}) - -appl.def('start_loading', function(){ - appl.def('loading', true) -}) - -appl.def('update_donation__success', function() { - appl.ajax_payment_details.fetch(appl.payment_details.data.id) - appl.def('loading', false) - // appl.close_modal() - appl.notify('Donation successfully updated!') -}) - -appl.def('delete_offline_donation', function() { - var payment = appl.payment_details.data - request - .del('/nonprofits/' + app.nonprofit_id + '/payments/' + payment.id) - .perform() - .then(function(resp) { - appl.notify("That offsite payment has been successfully deleted.") - appl.close_side_panel() - appl.payments.index() - }) -}) - -function fetch_refunds(charge_id) { - if(!charge_id) return - request.get('/nonprofits/' + app.nonprofit_id + "/charges/" + charge_id + "/refunds") - .perform() - .then(function(resp) { - appl.def('payment_details.refunds', resp.body) - }) -} - -function get_payment_campaign(payment) { - return payment && payment.donation && payment.donation.campaign -} - -appl.def('resend_receipt', function(type) { - var payment = appl.payment_details.data - appl.def('loading', true) - var url = `/nonprofits/${app.nonprofit_id}/payments/${payment.id}/resend_${type}_receipt` - var message = type === 'donor' - ? `Donation receipt emailed to ${app.user.email}` - : `Donation receipt emailed to you` - request.post(url) - .perform() - .then(function(resp) { - appl.def('loading', false) - appl.notify(message) - }) -}) - -// Format the JSON for a serialized dedication, which can have supporter_id, note, and type (honor/memory) -appl.def('format_dedication', function(dedic, node) { - var td = appl.prev_elem(node) - if(!td) return - var inner = '' - if (dedic) { - var json - try { json = JSON.parse(dedic) } catch(e) {} - if(json) { - let supporter_link = (json.supporter_id && json.supporter_id != '') ? - `${json.name}` : - json.name - inner = ` - Donation made in ${json.type || 'honor'} of - ${supporter_link}. - ${json.note ? `
Note: ${json.note}.` : ''} - ` - } else { - // Print plaintext dedication - inner = '' - } - } - td.innerHTML = inner -}) diff --git a/client/js/nonprofits/payments/index/tour.js b/client/js/nonprofits/payments/index/tour.js deleted file mode 100644 index 7014dbe5..00000000 --- a/client/js/nonprofits/payments/index/tour.js +++ /dev/null @@ -1,45 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../../common/vendor/bootstrap-tour-standalone') - -var transactions_tour = new Tour({ - backdrop: false, - steps: [ - { - orphan: true, - title: 'Welcome to your payments history dashboard!', - content: "This page shows your complete payments history. This includes donations through your website, campaign contributions, tickets for events, and offline checks." - }, - { - element: '.tour-filter', - placement: 'right', - title: 'Filtering & searching', - content: "Filter your payments using this panel. You can also use the search bar at the top to search by donor name or email.", - onHide: appl.close_filter_panel, - onShow: appl.open_filter_panel, - }, - { - element: '.tour-totalPayments', - placement: 'bottom', - title: 'Pending balance', - content: "This is your organization's pending balance. This amount is held temporarily in escrow until it is withdrawn into your organization's bank account." - }, - { - element: '.tour-payouts', - placement: 'bottom', - title: 'Payouts dashboard', - content: "To setup payouts and to see your payout history, you can click on this tab." - }, - { - orphan: true, - title: 'Check back!', - content: "As your organization starts to receive donations and contributions and to sell event tickets, check back here to watch your pending balance increase. Please contact support@commitchange.com if you have questions." - } - ] -}) - -if($.cookie('tour_transactions') === String(app.nonprofit_id)) { - $.removeCookie('tour_transactions', {path: '/'}) - transactions_tour.init() - transactions_tour.restart() -} - diff --git a/client/js/nonprofits/payments_chart.js b/client/js/nonprofits/payments_chart.js deleted file mode 100644 index 9a971d03..00000000 --- a/client/js/nonprofits/payments_chart.js +++ /dev/null @@ -1,111 +0,0 @@ -// License: LGPL-3.0-or-later -const request = require('../common/client') -const R = require('ramda') -const Chart = require('chart.js') -const Pikaday = require('pikaday') -const moment = require('moment') -const chartOptions = require('../components/chart-options') - -var frontendFormat = 'M/D/YYYY' -var backendFormat = 'YYYY-MM-DD' - -// set the default query to get the last year of payments -// and group them by month -var defaultParams = { - endDate: moment().format(backendFormat) - , startDate: moment().subtract(12, 'months').format(backendFormat) - , timeSpan: 'month' -} - -var pickadayDefaults = {format: frontendFormat, setDefaultDate: true} - -appl.def('updateChartParams', function(formObj) { - updateChart({ - endDate: moment(formObj.endDate).format(backendFormat) - , startDate: moment(formObj.startDate).format(backendFormat) - , timeSpan: formObj.timeSpan - }) -}) - -// start date Pickaday -new Pikaday(R.merge({ - field: document.getElementById('js-paymentsChart-startDate') - , maxDate: moment().subtract(1, 'week').toDate() - , defaultDate: moment().subtract(1, 'years').toDate() -}, pickadayDefaults)) - -// end date Pickaday -new Pikaday(R.merge({ - field: document.getElementById('js-paymentsChart-endDate') - , maxDate: moment().toDate() - , defaultDate: moment().toDate() -}, pickadayDefaults)) - -var ctx = document.getElementById('js-paymentsChart').getContext('2d') - -var chart = new Chart(ctx, { - type: 'bar' -, options: chartOptions.dollars -, data: {labels: [], datasets: []} -}) - -var url = `/nonprofits/${app.nonprofit_id}/payment_history` - -function updateChart(params) { - appl.def('loading_chart', true) - request.get(url) - .query(params) - .end(function(err, resp) { - chart.data.labels = formatLabels(R.pluck('time_span', resp.body), params.timeSpan) - chart.data.datasets = formatDatasets(resp.body) - chart.update() - appl.def('loading_chart', false) - }) -} - -function formatLabels(dates, type) { - switch (type) { - case "year": - return R.map(st => moment(st).format('YYYY'), dates) - case "month": - return R.map(st => moment(st).format('MMM YYYY'), dates) - case "week": - return R.map(st => - `${moment(st).format('M/D/YY')} - ${moment(st).add(7, 'days').format('M/D/YY')}` - , dates) - default: - return R.map(st => moment(st).format(frontendFormat), dates) - } -} - -const formatDatasets = (data) => [ - dataset('One time' - , 'onetime_cents' - , '66, 179, 223' - , data) - , dataset('Recurring' - , 'recurring_cents' - , '240, 205, 108' - , data) - , dataset('Tickets' - , 'tickets_cents' - , '238, 132, 128' - , data) - , dataset('Total' - , 'total_cents' - , '195, 195, 195' - , data) - ] - -function dataset(label, key, rgb, data) { - return { - label: label - , data: R.pluck(key, data) - , borderWidth: 1 - , borderColor: `rgb(${rgb})` - , backgroundColor: `rgba(${rgb},0.3)` - } -} - -updateChart(defaultParams) - diff --git a/client/js/nonprofits/payouts/create.js b/client/js/nonprofits/payouts/create.js deleted file mode 100644 index 673f85e0..00000000 --- a/client/js/nonprofits/payouts/create.js +++ /dev/null @@ -1,18 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/super-agent-promise') - -module.exports = create_payout - -function create_payout(form_obj, ui) { - ui.start() - return request.post('/nonprofits/' + app.nonprofit_id + '/payouts').send({payout: form_obj}).perform() - .then(function(resp) { - ui.success(resp) - return resp - }) - .catch(function(resp) { - ui.fail(resp) - return resp - }) -} - diff --git a/client/js/nonprofits/payouts/index/identity-verification-form.es6 b/client/js/nonprofits/payouts/index/identity-verification-form.es6 deleted file mode 100644 index f1bda0aa..00000000 --- a/client/js/nonprofits/payouts/index/identity-verification-form.es6 +++ /dev/null @@ -1,277 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const snabbdom = require('snabbdom') -const R = require('ramda') -const flyd = require('flyd') -const render = require('ff-core/render') -flyd.flatMap = require('flyd/module/flatmap') -const validatedForm = require('ff-core/validated-form') -const modal = require('ff-core/modal') -const button = require('ff-core/button') -const notification = require('ff-core/notification') -// local -const request = require('../../../common/request') -const geography = require('../../../common/geography') -const stateSelect = require('../../../components/state-selector') - - -// Form validation config for the normal info form -var messages = { - address: {state: 'Please enter a US state.'} -, business_tax_id: 'This should be 9 digits.' -} -var constraints = { - dob: { - day: { required: true , isNumber: true , min: 1 , max: 31 } - , month: { required: true , isNumber: true , min: 1 , max: 12 } - , day: { required: true , isNumber: true , min: 1900 , max: 2000 } - } -, first_name: {required: true} -, last_name: {required: true} -, address: { - city: {required: true} - , state: {required: true, includedIn: geography.stateCodes} - , line1: {required: true} - , postal_code: {required: true} - } -, business_tax_id: {required: true, format: /\d\d[- ]?\d\d\d\d\d\d\d/} -, ssn_last_4: {required: true, lenghtEquals: 4} -, phone_number: {required: true, format: /^\(?\d\d\d\)?[- ]*\d\d\d[- ]*\d\d\d\d$/} -} - -// Form validation config for the escalated form -var constraints_escalated = { - personal_id_number: {required: true, format: /\d\d\d[- ]?\d\d[- ]?\d\d\d\d/} -} -var messages_escalated = { - personal_id_number: {format: 'This should be 9 digits'} -} - - -function init() { - var state = { - regularForm: validatedForm.init({constraints, messages}) - , escalatedForm: validatedForm.init({constraints: constraints_escalated, messages: messages_escalated}) - , nonprofit: app.nonprofit - , modalID$: flyd.stream() - , error$: flyd.stream() - } - - const submitSuccess$ = flyd.merge(state.regularForm.validData$, state.escalatedForm.validData$) - const submitPath = `/nonprofits/${app.nonprofit_id}/verify_identity` - const resp$ = flyd.flatMap( - data => flyd.map(R.prop('body'), request({method: 'put', path: submitPath, send: {legal_entity: data}}).load) - , submitSuccess$ ) - flyd.map(()=> location.reload(), resp$) - const message$ = flyd.map(() => 'Successfully submitted! Reloading...', resp$) - state.notification = notification.init({message$}) - - state.loading$ = flyd.mergeAll([ - flyd.map(()=> true, submitSuccess$) - , flyd.map(()=> false, resp$) - ]) - - const status = app.nonprofit.verification_status - var n = document.querySelector('.js-openVerificationModal') - if(n) n.addEventListener('click', ev => status === 'escalated' ? state.modalID$('escalatedDialogModal') : state.modalID$('identityVerificationModal')) - - return state -} - - -const view = state => { - return h('div.verification', [ - normalDialog(state) - , escalatedDialog(state) - , formModal(state) - , escalatedForm(state) - , notification.view(state.notification) - ]) -} - - -const normalDialog = state => { - var body = h('div.modal-body', [ - h('p', 'Please complete this form to verify your identity. This information serves as an extra security measure to prevent fraud and is required by our payment processor to comply with KYC ("Know Your Customer") laws in the US.') - , h('p', 'All information submitted through this form is 256-bit SSL encrypted and is kept completely private.') - , h('p', "This information can be entered by any 'account representative' at your organization; someone from your organization who is an administrator of your CommitChange account.") - , h('hr') - , h('button.button--large', { on: {click: [state.modalID$, 'verificationFormModal']} }, ["Let's do it ", h('i.fa.fa-arrow-right')]) - ]) - - return modal({ - thisID: 'identityVerificationModal' - , id$: state.modalID$ - , title: h('h4', 'Account Identity Verification') - , body - }) -} - - -const formModal = state => { - return modal({ - thisID: 'verificationFormModal' - , id$: state.modalID$ - , title: 'Account Verification Form' - , body: formView(state) - }) -} - - -const escalatedDialog = state => { - var body = h('div', [ - h('p', `Our payment processor has requested the full social security number for the account holder. This usually happens when the given name or DOB does not exactly match the record in the social security database.`) - , h('p', `Entering the full social security number will almost always get your account verified.`) - , h('p', `Alternatively, you can re-enter the information on the basic form to either fix your information or use a different person at your org.`) - , h('hr') - , h('div.u-centered', [ - h('div', [ h('a.button', {on: {click: [state.modalID$, 'escalatedFormModal']}}, 'Enter full SSN') ]) - , h('p', 'or...') - , h('div', [ h('a.button', {on: {click: [state.modalID$, 'verificationFormModal']}}, 'Retry the basic form to correct any mistakes')]) - ]) - ]) - - return modal({ - thisID: 'escalatedDialogModal' - , id$: state.modalID$ - , title: 'Further Verification Needed' - , body - }) -} - - -const escalatedForm = state => { - var valForm = validatedForm.form(state.escalatedForm) - var field = validatedForm.field(state.escalatedForm) - - var body = valForm(h('form', {on: {submit: state.submit$}}, [ - h('label', [h('i.fa.fa-lock'), ' Full Social Security Number']) - , field(h('input', {props: {name: 'personal_id_number', type: 'text', placeholder: '9-digit number'}})) - , button({loading$: state.loading$, error$: state.error$}) - ])) - - return modal({ - thisID: 'escalatedFormModal' - , id$: state.modalID$ - , className: 'modal--flush' - , title: 'Identity Verification' - , body - }) -} - - -const formView = state => { - var field = validatedForm.field(state.regularForm) - var np = state.nonprofit - var formEl = h('form', {on: {submit: state.submit$}}, [ - h('p', [h('strong', 'Org Name: '), np.name]) - , h('div', [ - h('fieldset', {style: {display: 'inline-block', width: '48%', marginRight: '10px'}}, [ - h('label', 'Org Address') - , field(h('input', { style: {width: '95%'}, props: { type: 'text', name: 'address[line1]', value: np.address } })) - ]) - , h('fieldset', {style: {display: 'inline-block', width: '48%', marginRight: '10px'}}, [ - h('label', 'City') - , field(h('input', { props: { type: 'text', name: 'address[city]', value: np.city } })) - ]) - ]) - - , h('div', [ - h('fieldset', {style: {display: 'inline-block', width: '48%', marginRight: '10px'}}, [ - h('label', 'State') - , field(stateSelect({default: np.state_code, name: 'address[state]', value: np.state_code})) - ]) - , h('fieldset', {style: {display: 'inline-block', width: '48%', marginRight: '10px'}}, [ - h('label', 'Postal Code') - , field(h('input', { props: { type: 'text', name: 'address[postal_code]', value: np.zip_code } })) - ]) - ]) - - , h('div', [ - h('fieldset', {style: {display: 'inline-block', width: '48%', marginRight: '10px'}}, [ - h('label', 'Org Phone') - , field(h('input', {props: {type: 'text', name: 'phone_number', value: np.phone}})) - ]) - , h('fieldset', {style: {display: 'inline-block', width: '48%'}}, [ - h('label', 'Organization EIN') - , field(h('input', { props: { name: 'business_tax_id', value: np.ein } })) - ]) - ]) - - , h('hr') - , h('h6', 'Account Holder Info') - - , h('div', [ - h('fieldset.col-6', [ - h('label', 'First Name') - , field(h('input', {style: {width: '95%'}, props: {type: 'text', name: 'first_name'}})) - ]) - , h('fieldset.col-right-6', [ - h('label', 'Last Name') - , field(h('input', {props: {type: 'text', name: 'last_name'}})) - ]) - ]) - - , h('div', [ - dobField(state) - , h('fieldset.col-right-6', [ - h('label', 'Last 4 of Social Security Number') - , field(h('input', {props: {type: 'text', name: 'ssn_last_4'}})) - ]) - ]) - - , h('hr') - , h('p.finePrint.u-centered', [ - 'CommitChange processes payments using Stripe. By clicking "Submit" below, you agree to ' - , h('a', {props: {target: '_blank', href: 'https://stripe.com/connect/account-terms'}}, "Stripe's Connected Account Agreement.") - ]) - - , h('hr') - , h('div.u-centered', [ button({loading$: state.loading$, error$: state.error$}) ]) - ]) - - return validatedForm.form(state.regularForm, formEl) -} - - -// Generate a select element with a set of vals for options, a name attr, and a default option that has a null val -const selector = R.curry((name, vals, defaultOption) => - h('select', {props: {name: name}}, - R.prepend( - h('option', {props: {value: null, selected: true}}, defaultOption) - , R.map(n => h('option', {props: {value: n}}, n), vals) - ) - ) -) - - -const dobField = R.curry(state => { - var field = validatedForm.field(state.regularForm) - var fieldStyle = {width: '30%', display: 'inline-block'} - return h('fieldset.col-6', [ - h('label', 'Date of Birth') - , h('div', {style: R.merge(fieldStyle, {width: '28%'})}, [ - field(selector('dob[day]', R.range(1,32), 'Day')) - ]) - , h('strong', '/') - , h('div', {style: R.merge(fieldStyle, {width: '32%'})}, [ - field(selector('dob[month]', R.range(1, 13), 'Month')) - ]) - , h('strong', '/') - , h('div', {style: fieldStyle}, [ - field(selector('dob[year]', R.range(1900, 2000), 'Year')) - ]) - ]) -}) - - -// -- Render -var container = document.querySelector('.js-flimflam-verification') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -render({state: init(), container, view, patch}) - diff --git a/client/js/nonprofits/payouts/index/page.js b/client/js/nonprofits/payouts/index/page.js deleted file mode 100644 index 72704131..00000000 --- a/client/js/nonprofits/payouts/index/page.js +++ /dev/null @@ -1,27 +0,0 @@ -// License: LGPL-3.0-or-later -var create_payout = require('../create') -var format_err = require('../../../common/format_response_error') -appl.verify_identity = require('./verify_identity') -appl.create_bank_account = require('../../../bank_accounts/create.es6') -require('../../../bank_accounts/resend_confirmation_email') - -appl.def('create_payout', function(form_obj) { - create_payout(form_obj, new_payout_ui) -}) - - -var new_payout_ui = { - start: function() { - appl.is_loading() - }, - success: function(resp) { - appl.notify("Payout creation successful! Reloading page...") - appl.reload() - }, - fail: function(resp) { - appl.not_loading() - appl.def('error', format_err(resp)) - } -} - -require('./identity-verification-form.es6') diff --git a/client/js/nonprofits/payouts/index/verify_identity.js b/client/js/nonprofits/payouts/index/verify_identity.js deleted file mode 100644 index 140d3f8f..00000000 --- a/client/js/nonprofits/payouts/index/verify_identity.js +++ /dev/null @@ -1,24 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/super-agent-promise') -var format_err = require('../../../common/format_response_error') - -module.exports = verify_identity - -function verify_identity(form_obj) { - appl.def("identity_verification", {loading: true, error: ""}) - return request.put("/nonprofits/" + app.nonprofit_id + "/verify_identity") - .send({legal_entity: form_obj}).perform() - .then(function(resp) { - appl.def("identity_verification.loading", false) - appl.notify("Thank you! Your identity verification form was successfully saved.") - appl.close_modal() - appl.reload() - return resp - }) - .catch(function(resp) { - appl.def("identity_verification", { - loading: false, - error: format_err(resp) - }) - }) -} diff --git a/client/js/nonprofits/recurring_donations/index/create.js b/client/js/nonprofits/recurring_donations/index/create.js deleted file mode 100644 index 79eb716c..00000000 --- a/client/js/nonprofits/recurring_donations/index/create.js +++ /dev/null @@ -1,122 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../../components/wizard') -var format_err = require('../../../common/format_response_error') -var format = require('../../../common/format') -var request = require('../../../common/super-agent-promise') -var create_donation = require('../../../donations/create') -var create_card = require('../../../cards/create') -var formToObj = require('../../../common/form-to-object') - -var wiz = {} - - // Set the wizard's donation object to the form data - // amount, interval, time_unit, designation -wiz.set_donation = function(node) { - var data = formToObj(appl.prev_elem(node)) - var rd = data.recurring_donation - if(rd.start_date) { - rd.start_date = format.date.toStandard(rd.start_date) - } - if(rd.end_date) { - rd.end_date = format.date.toStandard(rd.end_date) - } - if(data.dollars) { - data.amount = format.dollarsToCents(data.dollars) - delete data.dollars - } - appl.def('rd_wizard.donation', data) - appl.wizard.advance('rd_wizard') -} - -// Save the supporter info. Advance immediately but save the promise. -wiz.save_supporter = function(form_obj) { - appl.wizard.advance('rd_wizard') - appl.rd_wizard.save_supporter_promise = request.post('/nonprofits/' + ENV.nonprofitID + '/supporters') - .send({supporter: form_obj}).perform() - .then(set_supporter_data) - .catch(show_err) -} - -// Resume on the supporter post promise, create a card, then create the donation with nested recurring donation -wiz.send_payment = function(card_obj) { - if(appl.rd_wizard.loading) return - appl.def('rd_wizard', {loading: true, error: ''}) - return appl.rd_wizard.save_supporter_promise - .then(function(supporter) { - return create_card({type: 'Supporter', id: supporter.id}, card_obj) - }) - .then(function(card) { - appl.rd_wizard.donation.token = card.token - return request.post('/nonprofits/' + ENV.nonprofitID + '/recurring_donations') - .send({ recurring_donation: appl.rd_wizard.donation }).perform() - }) - .then(complete_wizard) - .catch(show_err) -} - -// To be called on payment completion and a new recurring donation was successfully created -function complete_wizard() { - appl.notify("Successfully created! Reloading page...") - appl.def('loading', false) - setTimeout(()=> window.location.reload(), 1000) -} - -appl.def('rd_wizard', wiz) - -// Set the supporter values from a response to the wizard's data -function set_supporter_data(resp) { - appl.def('rd_wizard.donation', { - supporter_id: resp.body.id - }) - return resp.body -} - -// Set a general error on the wizard from an ajax response, displayed on any step -function show_err(resp) { - appl.def('rd_wizard.loading', false) - appl.def('rd_wizard.error', format_err(resp)) - throw new Error(resp) -} - -// Set all the default values for the data used in the recurring donation wizard -function set_defaults() { - appl.def('rd_wizard.donation', null) - appl.def('rd_wizard', { - donation: { - nonprofit_id: ENV.nonprofitID, - recurring_donation: { - interval: 1, - time_unit: 'month' - } - } - }) -} - -// Initialize wizard defaults -set_defaults() - - -// Initialize the pikaday date picker inputs in the various fields on the page -// jank -var Pikaday = require('pikaday') -var moment = require('moment') -var el = $('#newRecurringDonationModal') -el.find('input[name="recurring_donation.start_date"]').val(moment().format('MM-DD-YYYY')) -new Pikaday({ - field: el.find('input[name="recurring_donation.start_date"]')[0], - format: 'M/D/YYYY', - minDate: moment().toDate() -}) - -new Pikaday({ - field: el.find('input[name="recurring_donation.end_date"]')[0], - format: 'M/D/YYYY', - minDate: moment().toDate() -}) - -new Pikaday({ - field: document.querySelector('#edit_end_date'), - format: 'M/D/YYYY', - minDate: moment().toDate() -}) - diff --git a/client/js/nonprofits/recurring_donations/index/delete.js b/client/js/nonprofits/recurring_donations/index/delete.js deleted file mode 100644 index 42f8649d..00000000 --- a/client/js/nonprofits/recurring_donations/index/delete.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later - -appl.def('ajax_details', { - del: function(id, node) { - appl.ajax.del('recurring_donation_details', id, node).then(function(resp) { - appl.ajax.index('recurring_donations') - appl.notify("Successfully deactivated") - appl.close_side_panel() - }) - } -}) diff --git a/client/js/nonprofits/recurring_donations/index/index.es6 b/client/js/nonprofits/recurring_donations/index/index.es6 deleted file mode 100644 index b32e703a..00000000 --- a/client/js/nonprofits/recurring_donations/index/index.es6 +++ /dev/null @@ -1,74 +0,0 @@ -// License: LGPL-3.0-or-later -const snabbdom = require('snabbdom') -const flyd = require('flyd') -const h = require('snabbdom/h') -const R = require('ramda') -const modal = require('ff-core/modal') -const render = require('ff-core/render') -const request = require('../../../common/request') - -function init() { - var state = {} - state.modalID$ = flyd.stream() - state.data$ = flyd.map(R.prop('body'), request({ - path: `/nonprofits/${app.nonprofit_id}/recurring_donation_stats` - , method: 'get' - }).load) - - document - .querySelector('.js-openStatsModal') - .addEventListener('click', ev => state.modalID$('statsModal')) - - return state -} - -function view(state) { - if(!state.data$()) return h('div') - - var body = h('table.table', [ - h('tbody', [ - h('tr', [ - h('td', [h('strong', 'Active')]) - , h('td', `${state.data$().active_count} Donations`) - , h('td', `${state.data$().active_sum} Total`) - ]) - , h('tr', [ - h('td', [h('strong', 'Average Amount')]) - , h('td', `${state.data$().average} per month`) - , h('td', '') - ]) - , h('tr', [ - h('td', [h('strong', 'Cancelled')]) - , h('td', `${state.data$().cancelled_count} Donations`) - , h('td', `${state.data$().cancelled_sum} Total`) - ]) - , h('tr', [ - h('td', [h('strong', 'Charge Failures')]) - , h('td', `${state.data$().failed_count} Donations`) - , h('td', `${state.data$().failed_sum} Total`) - ]) - ]) - ]) - - return h('div', [ - modal({ - title: h('h4', 'Recurring Donations') - , id$: state.modalID$ - , thisID: 'statsModal' - , body - }) - ]) -} - - -// -- Render -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -var container = document.querySelector('.js-flimflamContainer') -var state = init() -render({patch, view, container, state}) - diff --git a/client/js/nonprofits/recurring_donations/index/page.js b/client/js/nonprofits/recurring_donations/index/page.js deleted file mode 100644 index a504de14..00000000 --- a/client/js/nonprofits/recurring_donations/index/page.js +++ /dev/null @@ -1,50 +0,0 @@ -// License: LGPL-3.0-or-later -require('./index.es6') -require('./create') -require('./update') -require('./delete') -require('../../../common/restful_resource') -require('../../../common/vendor/bootstrap-tour-standalone') -require('../../../common/panels_layout') -var format = require('../../../common/format') -appl.def('is_usa', format.geography.isUS) -require('./tour') - -appl.def('readable_interval', require('../readable_interval')) - -appl.def('recurring_donations', { - query: {page: 1}, - concat_data: true -}) - -appl.def('recurring_donations.index', function() { - appl.def('loading', true) - return appl.ajax.index('recurring_donations').then(function(resp) { - appl.def('loading', false) - if(appl.recurring_donations.query.page > 1) { - var main_panel = document.querySelector('.mainPanel') - main_panel.scrollTop = main_panel.scrollHeight - } - return resp - }) -}) - -appl.recurring_donations.index() - - - -appl.def('recurring_donation_details', { - resource_name: 'recurring_donations' -}) - - -appl.def('ajax_details', { - fetch: function(id, node) { - appl.def('loading', true) - appl.ajax.fetch('recurring_donation_details', id).then(function(resp) { - appl.open_side_panel(node) - appl.def('loading', false) - }) - }, -}) - diff --git a/client/js/nonprofits/recurring_donations/index/tour.js b/client/js/nonprofits/recurring_donations/index/tour.js deleted file mode 100644 index 658977c8..00000000 --- a/client/js/nonprofits/recurring_donations/index/tour.js +++ /dev/null @@ -1,40 +0,0 @@ -// License: LGPL-3.0-or-later -var tour_subscribers = new Tour({ - backdrop: false, - steps: [ - { - orphan: true, - title: 'Welcome to your recurring payments dashboard!', - content: "This is where all of your recurring donations will automatically appear. You can also manually add new recurring donations here with a few easy steps." - }, - { - element: '.tour-totalRecurring', - title: 'Monthly total', - placement: 'bottom', - content: 'Your recurring donations per month will be totaled here. Even if the donations are quarterly or annual, they will be calculated into this monthly balance.' - }, - { - element: '.tour-export', - placement: 'left', - title: 'Export', - content: 'If you need a report of your subscribers, use this Export button. It will download an excel file of all recurring donors.' - }, - { - element: '.tour-newSubscriber', - placement: 'left', - title: 'New subscriber button', - content: "To manually create a new custom recurring donation, use this button. You can specify the time interval as biweekly, monthly, quarterly, annual, or anything else. It's very flexible!" - }, - { - orphan: true, - title: 'Get fundraising!', - content: "Check back to this page to see your monthly total increase. Please contact support@commitchange.com if you have any questions." - } - ] -}) - -if($.cookie('tour_subscribers') === String(app.nonprofit_id)) { - $.removeCookie('tour_subscribers', {path: '/'}) - tour_subscribers.init() - tour_subscribers.restart() -} diff --git a/client/js/nonprofits/recurring_donations/index/update.js b/client/js/nonprofits/recurring_donations/index/update.js deleted file mode 100644 index 9f88a568..00000000 --- a/client/js/nonprofits/recurring_donations/index/update.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later - -appl.def('ajax_details', { - update: function(id, form_obj, node) { - appl.def('loading', true) - appl.ajax.update('recurring_donation_details', id, form_obj).then(function(resp) { - appl.def('loading', false) - appl.ajax.index('recurring_donations') - appl.notify('Successfully updated!') - appl.close_modal() - appl.ajax_details.fetch(appl.recurring_donation_details.id) - }) - } -}) - diff --git a/client/js/nonprofits/recurring_donations/readable_interval.js b/client/js/nonprofits/recurring_donations/readable_interval.js deleted file mode 100644 index 068f78e6..00000000 --- a/client/js/nonprofits/recurring_donations/readable_interval.js +++ /dev/null @@ -1,13 +0,0 @@ -// License: LGPL-3.0-or-later -// Given a time interval (eg 1,2,3..) and a time unit (eg. 'day', 'week', 'month', or 'year') -// Convert it to a nice readable single interval word like 'daily', 'biweekly', 'yearly', etc.. -// If one of the above words don't exist, will return eg 'every 7 months' -module.exports = readable_interval -function readable_interval(interval, time_unit) { - if(interval === 1) return time_unit + 'ly' - if(interval === 4 && time_unit === 'year') return 'quarterly' - if(interval === 2 && time_unit === 'year') return 'biannually' - if(interval === 2 && time_unit === 'week') return 'biweekly' - if(interval === 2 && time_unit === 'month') return 'bimonthly' - else return 'every ' + appl.pluralize(Number(interval), time_unit + 's') -} diff --git a/client/js/nonprofits/reports/modal.js b/client/js/nonprofits/reports/modal.js deleted file mode 100644 index cba69b2d..00000000 --- a/client/js/nonprofits/reports/modal.js +++ /dev/null @@ -1,58 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const flyd_filter = require('flyd/module/filter') -const R = require('ramda') -const h = require('snabbdom/h') -const moment = require('moment') - -// Modal component for exporting reports - -// XXX Note: this can be generalized to be any report modal, but for now it is specific to end-of-year - -flyd.log = flyd.map(console.log.bind(console)) - -function init() { - var state = { - currentYear: moment().year() - , changeYear$: flyd.stream() - , submit$: flyd.stream() - } - const selectedYear$ = flyd_filter( - year => Number(year) <= state.currentYear && Number(year) >= 2012 - , flyd.merge( - flyd.map(ev => ev.currentTarget.value, state.changeYear$) - , flyd.stream(state.currentYear) - ) - ) - state.exportPath$ = flyd.map(year => `/nonprofits/${ENV.nonprofitID}/reports/end_of_year.csv?year=${year}`, selectedYear$) - return state -} - -function view(state) { - return h('div.modal', {props: {id: 'endOfYearReportModal'}}, [ - h('div.modal-header', [ h('h2', 'End-of-year report') ]) - , h('div.modal-body', [ - modalBody(state) - ]) - ]) -} - -const modalBody = state => { - return h('div', [ - h('p', 'Export donors who have given during a selected year, with their aggregated totals, averages, and itemized payments histories for that year.') - , h('label', 'Year') - , h('input', { - on: {change: state.changeYear$} - , props: { - type: 'number' - , placeholder: 'YYYY' - , value: state.currentYear - , min: 2012 - , max: state.currentYear - } - }) - , h('a.button', {props: {target: '_blank', href: state.exportPath$()}}, 'Download CSV Report') - ]) -} - -module.exports = {init, view} diff --git a/client/js/nonprofits/show/page.js b/client/js/nonprofits/show/page.js deleted file mode 100755 index 8e13c5cc..00000000 --- a/client/js/nonprofits/show/page.js +++ /dev/null @@ -1,88 +0,0 @@ -// License: LGPL-3.0-or-later -if (app.nonprofit.brand_color) { - require('../../components/branded_fundraising') -} - -require('../../common/image_uploader') -require('../../components/fundraising/add_header_image') - -if(app.current_user) { - require('../../campaigns/new/wizard') - require('../../events/new/wizard') -} - -if(app.current_nonprofit_user) { - var editable = require('../../common/editable') - editable($('.editable'), { - placeholder: "Enter your nonprofit's story and impact here. We strongly recommend that this section is filled out with at least 250 words. It will automatically save as you type.", - sticky: $('.editable').length > 0 - }) - require('./tour') - var create_info_card = require('../../supporters/info-card.es6') - - appl.def('todos_action', '/profile_todos') - var todos = require('../../components/todos') - todos(function(data) { - appl.def('todos.items', [ - {text: "Add logo", done: data['has_logo'], modal_id: 'settingsModal' }, - {text: "Add header image", done: data['has_background'], modal_id: 'uploadBackgroundImage' }, - {text: "Add summary", done: data['has_summary'], modal_id: 'settingsModal' }, - {text: "Add images", done: data['has_image'], modal_id: 'uploadCarouselImages' }, - {text: "Add highlights", done: data['has_highlight'], modal_id: 'settingsModal' }, - {text: "Add services and impact", done: data['has_services'], link: '#js-servicesAndImpact' } - ]) - }) -} - -// -- Flimflam - -const snabbdom = require('snabbdom') -const h = require('snabbdom/h') -const flyd = require('flyd') -const R = require('ramda') -const donateWiz = require('../../nonprofits/donate/wizard') -const modal = require('ff-core/modal') -const render = require('ff-core/render') -const branding = require('../../components/nonprofit-branding') - -function init() { - var state = {} - state.donateWiz = donateWiz.init(flyd.stream({})) - state.modalID$ = flyd.stream() - return state -} - -function view(state) { - return h('section.box-r', [ - h('aside', [ - h('a.button--jumbo u-width--full', { - style: {background: branding.dark} - , on: {click: [state.modalID$, 'donationModal']} - }, [ - `Donate to ${app.nonprofit.name}` - ]) - , h('div.donationModal', [ - modal({ - thisID: 'donationModal' - , id$: state.modalID$ - , body: donateWiz.view(state.donateWiz) - // , notCloseable: state.donateWiz.paymentStep.cardForm.loading$() - }) - ]) - ]) - ]) -} - - -// -- Render - -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -var container = document.querySelector('.ff-container') -var state = init() -render({container, view, patch, state}) - diff --git a/client/js/nonprofits/show/tour.js b/client/js/nonprofits/show/tour.js deleted file mode 100644 index ead71f06..00000000 --- a/client/js/nonprofits/show/tour.js +++ /dev/null @@ -1,25 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/vendor/bootstrap-tour-standalone') - -var profile_tour = new Tour({ - backdrop: false, - steps: [ - { - orphan: true, - title: 'Welcome to your nonprofit profile!', - content: "This is a public page where people can donate, create peer-to-peer campaigns and find out about your organization. The more you fill out this page, the richer your donors' experiences will be.", - }, - { - element: '.tour-admin', - placement: 'bottom', - title: 'Manage your profile', - content: "You can manage your profile by clicking on these buttons at the top of the page." - } - ] -}) - -if($.cookie('tour_profile') === String(app.nonprofit_id)) { - $.removeCookie('tour_profile', {path: '/'}) - profile_tour.init() - profile_tour.restart() -} diff --git a/client/js/nonprofits/supporter_form/index.es6 b/client/js/nonprofits/supporter_form/index.es6 deleted file mode 100644 index a9a4200e..00000000 --- a/client/js/nonprofits/supporter_form/index.es6 +++ /dev/null @@ -1,36 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const R = require('ramda') -const flatMap = require('flyd/module/flatmap') -const request = require('../../common/request') -const serialize = require('form-serialize') -require('../../components/address-autocomplete') - -const submit$ = flyd.stream() -document.querySelector('.js-submit') - .addEventListener('submit', ev => { - ev.preventDefault() - submit$(ev) - }) - -flyd.map(()=> appl.def('loading', true), submit$) - -const postRequest = ev => { - return request({ - method: "POST" - , path: `/nonprofits/${app.nonprofit_id}/custom_supporter` - , send: {supporter: serialize(ev.currentTarget, {hash: true})} - }).load -} - -const getReqBody = flyd.map(R.prop('body')) - -const response$ = getReqBody(flatMap(postRequest, submit$)) - -flyd.map(()=> { - document.querySelector('.finishedMessage').className = 'finishedMessage' - document.querySelector('.js-submit').className = 'js-submit hide' -}, response$) - -flyd.map(()=> appl.def('loading', false), response$) - diff --git a/client/js/nonprofits/supporter_form/page.js b/client/js/nonprofits/supporter_form/page.js deleted file mode 100644 index a3d1fc6e..00000000 --- a/client/js/nonprofits/supporter_form/page.js +++ /dev/null @@ -1,2 +0,0 @@ -// License: LGPL-3.0-or-later -require('./index.es6') diff --git a/client/js/nonprofits/supporters/create.js b/client/js/nonprofits/supporters/create.js deleted file mode 100644 index d170f3b8..00000000 --- a/client/js/nonprofits/supporters/create.js +++ /dev/null @@ -1,17 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/super-agent-promise') - -module.exports = create_supporter - -function create_supporter(form_obj, ui) { - ui.start() - return request.post('/nonprofits/' + app.nonprofit_id + '/supporters') - .send(form_obj).perform() - .then(function(resp) { - ui.success(resp) - return resp - }) - .catch(function(resp) { - ui.fail(show_err(resp)) - }) -} diff --git a/client/js/nonprofits/supporters/get_name.js b/client/js/nonprofits/supporters/get_name.js deleted file mode 100644 index e4650c1b..00000000 --- a/client/js/nonprofits/supporters/get_name.js +++ /dev/null @@ -1,5 +0,0 @@ -// License: LGPL-3.0-or-later -appl.def('get_supporter_name', function(supporter) { - if(!supporter) return '' - return supporter.name || supporter.email -}) diff --git a/client/js/nonprofits/supporters/import/index.es6 b/client/js/nonprofits/supporters/import/index.es6 deleted file mode 100644 index 27733ff3..00000000 --- a/client/js/nonprofits/supporters/import/index.es6 +++ /dev/null @@ -1,262 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const h = require('snabbdom/h') -const R = require('ramda') -const snabbdom = require('snabbdom') -const formSerialize = require('form-serialize') -const flyd = require('flyd') -const render = require('ff-core/render') -flyd.flatMap = R.curry(require('flyd/module/flatmap')) -flyd.filter = require('flyd/module/filter') -flyd.mergeAll = require('flyd/module/mergeall') -flyd.lift = R.curry(require('flyd/module/lift')) -flyd.switchLatest = require('flyd/module/switchlatest') -const modal = require('ff-core/modal') -const wizard = require('ff-core/wizard') -const notification = require('ff-core/notification') -const button = require('ff-core/button') -// local -const request = require('../../../common/request') -const fileInputStream = require('../../../common/file-input-stream') -const uploadFile = require('../../../common/direct-to-s3-upload.es6') -const fields = require('./regex-header-matchers') - -// The import modal UI -// Upload a CSV, match up the columns, and import! - -// open the real import modal with appl.open_modal('importModal') - -function init() { - var state = { - fileUpload$: flyd.stream() - , submitFields$: flyd.stream() - , submitImport$: flyd.stream() - , fileUploadEmail$: flyd.stream() - , error$: flyd.stream() // unused for now - } - - const fileContents$ = flyd.flatMap(ev => fileInputStream(ev.currentTarget), state.fileUpload$) - state.uploadInput$ = flyd.map(ev => ev.currentTarget, state.fileUpload$) - - // Find the first line of the CSV, which is the headers row. Get the second - // result from the match function, as that will be the parenthesized match - // group. - const headers$ = flyd.map(txt => txt.match(/^(.*)(\r?\n|\r)/)[1].split(','), fileContents$) - state.rowCount$ = flyd.map(txt => txt.match(/\r?\n|\r/g).length, fileContents$) - - // Stream of matched table/column fields based on running regexes over the haders of their files - // The matches are stored as pairs of [type, field], eg ['Supporter, 'First Name'] - state.matchedHeaders$ = flyd.map(findHeaderMatches, headers$) - - // state.submitImport$ is passed the current component state, and we just want a stream of input node objects for uploadFile - const uploaded$ = flyd.flatMap(uploadFile, state.submitImport$) - - // The matched headers with a simplified data structure to post to the server - // data structure is like {header_name => match_name} -- eg {'Donation Amount' => 'donation.amount'} - state.headerData$ = flyd.map(ev => formSerialize(ev.currentTarget, {hash: true}), state.submitFields$) - - - const importResp$ = flyd.switchLatest(flyd.lift(postImport, state.headerData$, uploaded$)) - - const emailFile$ = R.compose( - flyd.flatMap(uploadFile) - , flyd.map(ev => {ev.preventDefault(); return ev.currentTarget.querySelector('input')}) - )(state.fileUploadEmail$) - - state.loading$ = flyd.mergeAll([ - flyd.map(()=> true, state.submitImport$) // start loading - , flyd.map(()=> false, importResp$) // stop loading - , flyd.map(()=> true, state.fileUploadEmail$) - , flyd.map(()=> false, emailFile$) - ]) - - const notify$ = flyd.map( - ()=> 'Your import was successfully initiated. Feel free to upload additional files.' - , emailFile$ - ) - - // All streams that cause the wizard to advance - const wizardStep$ = flyd.mergeAll([ - flyd.stream(0) - , flyd.map(() => 1, state.fileUpload$) - , flyd.map(() => 2, state.submitFields$) - ]) - - const wizardCompleted$ = flyd.map(()=> true, importResp$) - - state.modalID$ = flyd.stream() - const jump$ = flyd.stream() - state.wizard = wizard.init({currentStep$: wizardStep$, isCompleted$: wizardCompleted$}) - state.notification = notification.init({message$: notify$}) - - // XXX using vanilla JS for the initial modal open action. This can be replaced with Flyd/Vdom when the CRM table meta is in vdom - var btnSuper = document.querySelector('.js-importButton') - if(btnSuper) btnSuper.addEventListener('click', ev => state.modalID$('importModal')) - - return state -} - - -// post to /imports after the file is uploaded to S3 -const postImport = R.curry((headers, file) => { - return flyd.map(R.prop('body'), request({ - method: 'post' - , path: `/nonprofits/${app.nonprofit_id}/imports` - , send: {file_uri: file.uri, header_matches: headers} - }).load) -}) - - -// Maps over the header strings. -// Return an array of pairs of matches like [tableName, fieldName] using -// regexes (from the fields object above) based on the column headers from the CSV -const findHeaderMatches = - R.map( - name => ({ - name: name - , match: R.find(f => R.test(f.regex, name), fields) - }) - ) - -function dontLetThemMessItUp(state) { - return modal({ - thisID: 'importDontLetThemDoIt' - , id$: state.modalID$ - , title: 'New Import' - , className: 'modal--flush' - , body: dontLetThemBody(state) - }) -} - -function dontLetThemBody(state) { - return h('div', [ - h('p', 'Upload a spreadsheet to get your import rolling. Imports will take 1-3 days depending on the data.') - , h('p', 'You can generally import any donor and supporter information along with their donation amounts, dates, designations, etc.') - , h('p', 'You will receive an email followup once the import is complete or if there were any problems with the data.') - , h('form', {on: {submit: state.fileUploadEmail$}}, [ - h('input', {props: {type: 'file', name: 'file'}}) - , h('hr') - , button({loading$: state.loading$, error$: state.error$}) - ]) - ]) -} - - -function view(state) { - var wiz = wizard.view(R.merge(state.wizard, { - steps: [ - { name: 'Upload', body: uploadStep(state) } - , { name: 'Fields', body: fieldsStep(state) } - , { name: 'Import', body: importStep(state) } - ] - , followup: finishedStep(state) - })) - - return h('div.import', [ - modal({ - thisID: 'importModal' - , id$: state.modalID$ - , title: 'New Import' - , noPad: true - , className: 'modal--flush' - , body: wiz - }) - , dontLetThemMessItUp(state) - , notification.view(state.notification) - ]) -} - - -const finishedStep = state => - h('div', [ - h('p.u-bold.u-color--green', 'Your import has successfully started.') - , h('p', "It'll take a few minutes to complete everything.") - , h('p', ["We'll send a notification message to your email at ", h('span.u-bold', app.user.email), " as soon as it's done."]) - , h('hr') - , h('div.u-centered', [ h('button.button', {on: {click: [state.modalID$, false]}}, 'Close') ]) - ]) - - -const uploadStep = state => - h('div', [ - h('p.u-bold', "First, let's upload a CSV file with the supporter and donation data you'd like to import. ") - , h('p', 'Make sure your file has column headers in the first row.') - , h('hr') - , h('form', [ - h('input', {on: {change: state.fileUpload$}, props: {type: 'file', name: 'file'}}) - ]) - ]) - - -// Modal for the user to match up CSV headers with database columns -function fieldsStep(state) { - if(!state.matchedHeaders$()) return h('div') - - return h('form', { - on: {submit: ev => {ev.preventDefault(); state.submitFields$(ev)}} - }, [ - h('p', "We've automatically detected your CSV headers. Please match up your file's column headers with our available fields.") - , h('table.table', [ - h('thead', h('tr', [h('td', 'CSV Column'), h('td', 'Import As...')])) - , h('tbody', R.map(colSelectRow, state.matchedHeaders$())) - ]) - , h('hr') - , h('div.u-centered', [ - h('button.button', 'Next') - ]) - ]) -} - - -const colSelectRow = header => - h('tr', [ - h('td', [h('strong', header.name)]) - , h('td', [h('i.fa.fa-long-arrow-right')]) - , h('td.u-padding--0', [ - h('select.u-margin--0.u-inlineBlock.u-width--full.u-marginY--5' - , { props: {name: header.name} } - , R.concat( - [ // Default options for every field - h('option', {props: {selected: !header.match, value: ''}}, 'Select Field') - , h('option', {props: {value: ''}}, 'Ignore') - , h('option', {props: {value: 'custom_field'}}, 'New Custom Field') - ] - , R.map(fieldOption(header), fields) - ) - ) - ]) - ]) - -const fieldOption = header => field => - h('option', { - props: { - value: field.import_key - , selected: header.match && header.match.name === field.name - } - }, field.name ) - - -const importStep = state => - h('div', [ - h('p', ['We will be importing the following data from ', h('strong', (state.rowCount$()-1) + ' rows'), ': ']) - , h('p.u-bold', R.join(', ', R.map(obj => obj.name, (state.matchedHeaders$() || [])))) - , h('p', "If this looks good to you, hit Submit to get the import rolling.") - , h('p', "Note that the import can always be undone later.") - , h('form.u-centered', { - on: { submit: ev => { ev.preventDefault(); state.submitImport$(state.uploadInput$())}} - }, [ button({loading$: state.loading$, error$: state.error$}) ]) - ]) - - - -// -- Render to the page - -var container = document.querySelector('#js-vdomParty') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -render({state: init(), view, container, patch}) - diff --git a/client/js/nonprofits/supporters/import/regex-header-matchers.js b/client/js/nonprofits/supporters/import/regex-header-matchers.js deleted file mode 100644 index c9a9ea27..00000000 --- a/client/js/nonprofits/supporters/import/regex-header-matchers.js +++ /dev/null @@ -1,121 +0,0 @@ -// License: LGPL-3.0-or-later - -// A full list of available import keys that data can be imported into -// import_key roughly translates to 'table_name.column', but not exactly... see insert_imports.rb -// Also, the regexes allow us to automatically detect what CSV headers match what import keys -// 'regex' is the regex that we use to match on the CSV header -// 'name' is the readable name of the import key / table.column -// 'import_key' is a key name that is used to handle the importing of the data, found in insert_imports.rb - -// Automatic header matching is performed top down -- the first regex to -// successfully match is used. So put the more generic matches at the bottom, -// below the more specific matches. - -module.exports = [ - { - regex: /.*e(-)?mail *(address)?.*/i - , name: 'Donor Email' - , import_key: 'supporter.email' - } - , { - regex: /.*country.*/i - , name: 'Donor Country' - , import_key: 'supporter.country' - } - , { - regex: /.*(street[ \-_]*)?address *(line[ \-_]*2).*/i - , name: 'Donor Address (line 2)' - , import_key: 'supporter.address_line2' - } - , { - regex: /.*(street[ \-_]*)?address *(line[ \-_]*1)?.*/i - , name: 'Donor Address (line 1)' - , import_key: 'supporter.address' - } - , { - regex: /.*city.*/i - , name: 'Donor City' - , import_key:'supporter.city' - } - , { - regex: /.*(state|province)[ \-_]*(code)?.*/i - , name: 'Donor State/Region' - , import_key:'supporter.state_code' - } - , { - regex: /.*(zip|postal)[ \-_]*(code)?.*/i - , name: 'Donor Postal Code' - , import_key:'supporter.zip_code' - } - , { - regex: /.*(tele)?phone *(number)?.*/i - , name: 'Donor Phone' - , import_key:'supporter.phone' - } - , { - regex: /.*(org|organization|company) *(name)?.*/i - , name: 'Donor Company/Org' - , import_key:'supporter.organization' - } - , { - regex: /.*(donation|contributed)?[ \-_]*(amount|total).*/i - , name: 'Donation Amount' - , import_key:'donation.amount' - } - , { - regex: /.*(fund|designation|towards).*/i - , name: 'Donation Designation/Fund' - , import_key:'donation.designation' - } - , { - regex: /.*(campaign)[ \-_]*(name)?.*/i - , name: 'Donation Campaign Name' - , import_key:'donation.designation' - } - , { - regex: /.*(honorarium|dedication|in honor of|memorium|in memory of).*/i - , name: 'Donation Memorium/Dedication' - , import_key:'donation.dedication' - } - , { - regex: /.*((date)|(created(_at)?)).*/i - , name: 'Donation Date' - , import_key:'donation.date' - } - , { - regex: /.*(payment)? *kind|type|method.*/i - , name: 'Donation Payment Method' - , import_key:'offsite_payment.kind' - } - , { - regex: /.*comment|note(s?).*/i - , name: 'Additional Note/Comment' - , import_key:'donation.comment' - } - , { - regex: /.*check.*/i - , name: 'Check Number' - , import_key:'offsite_payment.check_number' - } - , { - regex: /.*tag.*/i - , name: 'New Tag' - , import_key:'tag' - } - , { - regex: /.*(^ *(first[ \-_]*)?name).*/i - , name: 'Donor First Name' - , import_key:'supporter.first_name' - } - , { - regex: /.*(^ *(last[ \-_]*)?name).*/i - , name: 'Donor Last Name' - , import_key:'supporter.last_name' - } - , { - regex: /.*(full[ \-_]*)?name.*/i - , name: 'Donor Full Name' - , import_key:'supporter.name' - } -] - diff --git a/client/js/nonprofits/supporters/index/action_recipient.js b/client/js/nonprofits/supporters/index/action_recipient.js deleted file mode 100644 index df3a1c10..00000000 --- a/client/js/nonprofits/supporters/index/action_recipient.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -module.exports = action_recipient - -function action_recipient(){ - var total = appl.supporters.selecting_all ? appl.supporters.total_count : appl.supporters.selected.length - if (appl.supporters.selected.length <= 1) - return appl.supporters.selected[0].name || appl.supporters.selected[0].email - else - return total + ' Supporters' -} - diff --git a/client/js/nonprofits/supporters/index/bulk_delete.js b/client/js/nonprofits/supporters/index/bulk_delete.js deleted file mode 100644 index 27c2d172..00000000 --- a/client/js/nonprofits/supporters/index/bulk_delete.js +++ /dev/null @@ -1,40 +0,0 @@ -// License: LGPL-3.0-or-later -var action_recipient = require("./action_recipient") -var request = require('../../../common/client') - -appl.def('show_bulk_delete_supporters', function(){ - var total = appl.supporters.selecting_all ? appl.supporters.total_count : appl.supporters.selected.length - appl - .def('action_recipient', action_recipient()) - .def('supporters.selected_with_limit', appl.supporters.selected.slice(0,29)) - .def('supporters.remaining', total - appl.supporters.selected_with_limit.length) - .open_modal('bulkDeleteModal') -}) - - -appl.def('bulk_delete', function() { - - var post_data = {} - if (appl.supporters.selecting_all) - { - post_data.selecting_all = true - post_data.query = appl.supporters.query - } - else - { - post_data.supporter_ids = appl.supporters.selected.map(function(s) { return s.id }) - } - - appl.def('loading', true) - request.put("/nonprofits/" + app.nonprofit_id + "/supporters/bulk_delete") - .send(post_data) - .end(function(err, resp){ - appl.def('loading', false) - if(!resp.ok) return appl.notify('Sorry, we were unable to delete those supporters') - appl.notify('Supporters successfully removed') - appl.close_modal() - appl.supporters.index() - appl.def('supporters.selected', []) - }) -}) - diff --git a/client/js/nonprofits/supporters/index/import.js b/client/js/nonprofits/supporters/index/import.js deleted file mode 100644 index f7ee47bb..00000000 --- a/client/js/nonprofits/supporters/index/import.js +++ /dev/null @@ -1,9 +0,0 @@ -// License: LGPL-3.0-or-later - -appl.def('import_data', { - after_post: function(resp) { - appl - .open_modal("importCompletedModal") - .supporters.index() - } -}) diff --git a/client/js/nonprofits/supporters/index/list_supporters.js b/client/js/nonprofits/supporters/index/list_supporters.js deleted file mode 100644 index d0442899..00000000 --- a/client/js/nonprofits/supporters/index/list_supporters.js +++ /dev/null @@ -1,112 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flimflam/flyd') // for ajaxing /index_metrics, line 27 -const request = require('../../../common/request') // for ajaxing /index_metrics -var map = require('../../../components/maps/cc_map') -var npo_coords = require('../../../components/maps/npo_coordinates')() - -appl.def('supporters.selected', []) - -appl.def('supporters.index', function() { - appl.def('loading', true) - appl.ajax.index('supporters').then(function(resp) { - appl.supporters.open_side_panel_with_params() - if(appl.supporters.selecting_all){ - set_checked(resp.body.data, true) - } - appl.def('loading', false) - var supporter_ids = appl.supporters.data.map(function(datum){return datum.id}) - appl.def('supporters.data', appl.supporters.data.map(supp => { - supp.tags_remaining = (supp.tags && supp.tags.length > 5) - ? (supp.tags.length - 5) - : false - supp.tags = supp.tags ? supp.tags.slice(0,5) : [] - return supp - })) - map.init('specific-npo-supporters', {fit_all: true}, {npo_id: app.nonprofit.id, supporter_ids: supporter_ids}) - }) - - appl.def('metrics_loading', true) - const response$ = request({ - method: 'get' - , path: `/nonprofits/${ENV.nonprofitID}/supporters/index_metrics` - , query: appl.supporters.query - }).load - const respOk$ = flyd.filter(r => r.status === 200, response$) - flyd.map(r => { appl.def('metrics_loading', false) }, respOk$) - flyd.map(r => { appl.def('supporters', r.body) }, respOk$) -}) - - -appl.def('supporters', { - query: {page: 1}, - concat_data: true, - path_prefix: '/nonprofits/' + app.nonprofit_id + '/' -}) - - -appl.def('supporters.open_side_panel_with_params', function(){ - var url_supporter_id = utils.get_param('sid') - if(url_supporter_id) { - appl.ajax.fetch('supporter_details', url_supporter_id).then(function(resp){ - appl.supporter_details.show(resp.body.data) - appl.def('loading', false) - }) - } -}) - - -appl.supporters.index() - - -appl.def('toggle_select_all', function(node) { - var checkbox = appl.prev_elem(node) - appl.def('supporters.selecting_all', checkbox.checked) - if(checkbox.checked) { // select all - appl.def('supporters.data', set_checked(appl.supporters.data, true)) - appl.def('supporters.selected', appl.supporters.data) - } -}) - - -appl.def('toggle_select_page', function(node) { - var checkbox = appl.prev_elem(node) - appl.def('supporters.selecting_all', false) - if(checkbox.checked) { // select all - appl.def('supporters.data', set_checked(appl.supporters.data, true)) - appl.def('supporters.selected', appl.supporters.data) - } else { // deselect all - appl.def('supporters.data', set_checked(appl.supporters.data, false)) - appl.def('supporters.selected', []) - } - return appl -}) - - -appl.def('toggle_supporters_checkbox', function(id, node) { - var checked = appl.prev_elem(node).checked - appl.find_and_set('supporters.data', {id: id}, {is_checked: checked}) - appl.def('supporters.selected', appl.get_checked_supporters()) - appl.def('supporters.selecting_all', false) -}) - - -appl.def('get_checked_supporters', function() { - return appl.supporters.data.filter(function(s) { return s.is_checked }) -}) - - -appl.def('uncheck_all_supporters', function() { - appl.supporters.data.forEach(function(obj){return obj.is_checked = false}) - appl.def('supporters.data', appl.supporters.data) - .def('supporters.selected', []) - .def('supporters.selecting_all', false) -}) - - -function set_checked(supporters, state) { - return supporters.map(function(s) {s.is_checked = state; return s}) -} - -appl.def('print_last_payment_before', function(last_payment_before) { - return String(last_payment_before).split('_').join(' ') -}) diff --git a/client/js/nonprofits/supporters/index/manage_custom_fields.js b/client/js/nonprofits/supporters/index/manage_custom_fields.js deleted file mode 100644 index 378bb099..00000000 --- a/client/js/nonprofits/supporters/index/manage_custom_fields.js +++ /dev/null @@ -1,124 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/client') -var action_recipient = require('./action_recipient') -var fields = require('./tags_and_fields_shared_methods') -var type = 'custom_field' - -fields.index_masters(type) - -appl.def('custom_fields.masters.show_modal', function(){ - appl.open_modal('manageFieldMasterModal') -}) - - -appl.def('custom_fields.masters.add', function(form_obj, node){ - fields.add({ type: type, form_obj: form_obj, node: node }) -}) - - -appl.def('custom_fields.masters.delete', function(name, id, node) { - fields.delete({ name: name, id: id, type: type, node: node }) - appl.ajax.index('supporter_details.custom_fields') -}) - - -appl.def('custom_fields.bulk.show_modal', function(node) { - appl - .def('custom_fields.bulk.action_recipient', action_recipient()) - .open_modal('editBulkCustomFieldsModal') -}) - - -appl.def('custom_fields.bulk.toggle_remove', function(this_field, node) { - if (this_field.remove) this_field.remove = false; - else this_field.remove = true; - appl.def('custom_fields.masters.data', appl.custom_fields.masters.data) -}) - - -appl.def('custom_fields.bulk.prepare_to_post', function(form_obj, node) { - var fields = [] - - for(var i = 1, len = form_obj.id.length; i < len; ++i) { - if(form_obj.remove[i] === 'true') - fields.push({custom_field_master_id: form_obj.id[i], value: ''}) - else if(form_obj.val[i] === '') - {} - else - fields.push({custom_field_master_id: form_obj.id[i], value: form_obj.val[i]}) - } - - if(appl.supporters.selecting_all) - var post_data = { - custom_fields: fields, - selecting_all: true, - query: appl.supporters.query - } - else - var post_data = { - custom_fields: fields, - supporter_ids: appl.supporters.selected.map(function(s){return s.id}) - } - - post_custom_field_edits(post_data, function() { - appl - .notify('Successfully updated fields for ' + appl.custom_fields.bulk.action_recipient) - .uncheck_all_supporters() - }) - appl.def('custom_fields.masters.data', appl.custom_fields.masters.data.map(function(s) {s.remove = false; return s})) - appl.prev_elem(node).reset() -}) - - - -appl.def('custom_fields.single.show_modal', function(name, id, node) { - var custom_field_list = [] - - appl.custom_fields.masters.data.forEach(function(custom_field_master) { - var new_custom_field = { - id: custom_field_master.id, - name: custom_field_master.name - } - appl.supporter_details.custom_fields.data.forEach(function(custom_field_join) { - if(custom_field_join.name === custom_field_master.name && custom_field_join.value) - new_custom_field.value = custom_field_join.value - }) - custom_field_list.push(new_custom_field) - }) - - appl - .def('supporter_details.custom_field_list', custom_field_list) - .open_modal('editCustomFieldsModal') -}) - - - -appl.def('custom_fields.single.prepare_to_post', function(form_obj) { - var fields = [] - for(var i = 1, len = form_obj.id.length; i < len; ++i) { - fields.push({custom_field_master_id: form_obj.id[i],value: form_obj.val[i]}) - } - var post_data = { - custom_fields: fields, - supporter_ids: [appl.supporter_details.data.id] - } - - post_custom_field_edits(post_data, function() { - appl - .notify('Successfully updated fields for ' + appl.supporter_details.data.name_email_or_id) - .ajax.index('supporter_details.custom_fields') - }) -}) - -function post_custom_field_edits(post_data, callback){ - appl.def('loading', true) - request - .post('custom_field_joins/modify', post_data) - .end(function(err, resp) { - appl - .close_modal() - .def('loading', false) - callback() - }) -} - diff --git a/client/js/nonprofits/supporters/index/manage_tags.js b/client/js/nonprofits/supporters/index/manage_tags.js deleted file mode 100644 index 1ce48156..00000000 --- a/client/js/nonprofits/supporters/index/manage_tags.js +++ /dev/null @@ -1,137 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/client') -var action_recipient = require('./action_recipient') -var tags = require('./tags_and_fields_shared_methods') -var type = 'tag' - -tags.index_masters(type) - -appl.def('tags.masters.show_modal', function(){ - appl.open_modal('manageTagMasterModal') -}) - - -appl.def('tags.masters.add', function(form_obj, node){ - tags.add({type: type, form_obj: form_obj, node: node}) -}) - - -appl.def('tags.masters.delete', function(name, id, node) { - var cb = appl.supporters.index - tags.delete({ name: name, id: id, type: type, node: node, cb: cb}) -}) - - -appl.def('tags.bulk.show_modal', function(node){ - appl.tags.masters.data.map(function(s) {s.edit_action = null; return s}) - - appl - .def('tags.masters.data', appl.tags.masters.data) - .def('tags.bulk.action_recipient', action_recipient()) - .open_modal('bulkTagEditModal') -}) - - -// sets any selected tag's edit_action attribute to add, remove or null -appl.def('tags.bulk.add_or_remove', function(i, action, node){ - var this_tag = appl.tags.masters.data[i] - if (this_tag.edit_action === action) - this_tag.edit_action = null - else - this_tag.edit_action = action - - appl.def('tags.masters.data', appl.tags.masters.data) -}) - - -// creates an array of tag objects like tags = [{tag_master_id: 123, selected: true}, ...] -// which is passed as an attribute in post_data. -// post_data gets passed as an argument to the post_tag_edits function -// which handles the ajax stuff -appl.def('tags.bulk.prepare_to_post', function() { - var tags = [] - var post_data = {} - - appl.tags.masters.data.forEach(function(s) { - if(s.edit_action === 'add') - tags.push({tag_master_id: s.id, selected: true}) - else if(s.edit_action === 'remove') - tags.push({tag_master_id: s.id, selected: false}) - }) - - post_data.tags = tags - - if(appl.supporters.selecting_all) { - post_data.selecting_all = true - post_data.query = appl.supporters.query - } - else { - post_data.supporter_ids = appl.supporters.selected.map(function(s){return s.id}) - } - - post_tag_edits(post_data, function() { - appl - .notify('Successfully updated tags for ' + (appl.supporters.selecting_all ? 'All Supporters' : appl.tags.bulk.action_recipient)) - .def('supporters.selected', []) - }) -}) - - -appl.def('tags.single.show_modal', function(node){ - // creates a tag list that adds an is_checked key - // if the current supporter has that tag - var tag_list = [] - - appl.tags.masters.data.forEach(function(master_tag) { - var new_tag = { - id: master_tag.id, - name: master_tag.name - } - appl.supporter_details.tags.data.forEach(function(supporter_tag) { - if(supporter_tag.name === master_tag.name) - new_tag.is_checked = true - }) - tag_list.push(new_tag) - }) - - appl.def('supporter_details.tag_list', tag_list) - appl.open_modal('tagEditModal') -}) - - -appl.def('tags.single.prepare_to_post', function(form_obj) { - var tags = [] - for(var i = 1, len = form_obj.id.length; i < len; ++i) { - tags.push({ - tag_master_id: form_obj.id[i], - selected: form_obj.selected[i] - }) - } - var post_data = { - tags: tags, - supporter_ids: [appl.supporter_details.data.id] - } - - post_tag_edits(post_data, function() { - appl - .notify('Successfully updated tags for ' + appl.supporter_details.data.name_email_or_id) - .def('supporters.selected', []) - .ajax.index('supporter_details.tags') - }) -}) - - -function post_tag_edits(post_data, callback){ - appl.def('loading', true) - request - .post('tag_joins/modify', post_data) - .end(function(err, resp) { - if(!resp.ok) return appl.notify(utils.print_error(resp)) - appl - .close_modal() - .def('loading', false) - .supporters.index() - if(appl.supporter_details.data) appl.ajax_supporter.fetch(appl.supporter_details.data.id) - callback() - }) -} diff --git a/client/js/nonprofits/supporters/index/merge_supporters.js b/client/js/nonprofits/supporters/index/merge_supporters.js deleted file mode 100644 index c0b5177c..00000000 --- a/client/js/nonprofits/supporters/index/merge_supporters.js +++ /dev/null @@ -1,79 +0,0 @@ -// License: LGPL-3.0-or-later -var action_recipient = require("./action_recipient") -var request = require('../../../common/client') -require('../../../components/wizard') -var formatErr = require('../../../common/format_response_error') -const R = require('ramda') - -appl.def('merge.has_any', function(arr) { - var supporters = appl.merge.data.supporters - for(var i = 0, sup_len = supporters.length; i < sup_len; i++) { - for(var j = 0, arr_len = arr.length; j < arr_len; j++) { - var key = arr[j] - if(supporters[i][key]) { - appl.def('merge.data.has_at_least_one.' + key, true) - } - } - } -}) - -appl.def('merge.init', function(){ - if (appl.supporters.selected.length > 5) { - appl.notify("Sorry, you can't merge more than 5 records at a time.") - return - } - if (appl.supporters.selected.length < 2) { - appl.notify("Sorry, you need to select more than one record to merge.") - return - } - var ids = appl.supporters.selected.map(function(s) { return s.id }) - appl.def('loading', true) - appl.def('merge.data', '') - appl.def('merge.data.action_recipient', action_recipient()) - request.get('/nonprofits/' + app.nonprofit_id + '/supporters/merge_data') - .query({ids: ids}) - .end(function(err, res) { - appl.def('loading', false) - appl.def('merge.data.supporters', res.body) - appl.merge.has_any(['name', 'email', 'phone', 'address']) - appl.open_modal('mergeModal') - }) -}) - -appl.def('merge.set', function(form_obj, node) { - var supp = appl.merge.data.new_supporter - appl.def('merge.data.new_supporter', R.merge(supp, form_obj)) -}) - -appl.def('merge.select_address', function(supp, node) { - appl - .def('merge.data.new_supporter.address', supp.address) - .def('merge.data.new_supporter.city', supp.city) - .def('merge.data.new_supporter.state_code', supp.state_code) - .def('merge.data.new_supporter.zip_code', supp.zip_code ) - .def('merge.data.new_supporter.country', supp.country ) -}) - -appl.def('merge.submit', function(form_object, node){ - appl.def('loading', true) - - request.post("/nonprofits/" + app.nonprofit_id + "/supporters/merge") - .send({ - supporter: form_object, - supporter_ids: appl.supporters.selected.map(function(s){return s.id}) - }) - .end(function(err, resp){ - appl.def('loading', false) - if(resp.ok) { - appl - .def('supporters.selected', []) - .notify('Supporters successfully merged.') - .supporters.index() - } else { - appl.notify('Error: ' + formatErr(resp)) - } - }) - appl.close_modal() - appl.wizard.reset('merge_wiz') -}) - diff --git a/client/js/nonprofits/supporters/index/page.js b/client/js/nonprofits/supporters/index/page.js deleted file mode 100644 index f0e29292..00000000 --- a/client/js/nonprofits/supporters/index/page.js +++ /dev/null @@ -1,34 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../../common/restful_resource') -require('../../../common/panels_layout') -require('../../../components/date_range_picker') -require('../../../common/apply-pikaday') -require('./list_supporters') -require('./timeline') -require('./supporter_details') -require('./sidepanel') -require('./bulk_delete') -require('./manage_tags') -require('./manage_custom_fields') -require('../../../common/ajax/get_campaign_and_event_names_and_ids')(app.nonprofit_id) -require('./merge_supporters') -require('../import/index.es6') -require('../../../components/tables/filtering/apply_filter')('supporters') -require('./tour') - - -// Flim flam go: -require('../../../supporters') - - -// XXX cruft -appl.def('set_export_custom_fields', function(node) { - var checkbox = appl.prev_elem(node) - if (appl.supporters.query.export_custom_fields) { - appl.supporters.query.export_custom_fields += ',' - } else { - appl.supporters.query.export_custom_fields = '' - } - appl.supporters.query.export_custom_fields += checkbox.value - appl.def('supporters.query', appl.supporters.query) -}) diff --git a/client/js/nonprofits/supporters/index/sidepanel/generate-content.js b/client/js/nonprofits/supporters/index/sidepanel/generate-content.js deleted file mode 100644 index b392c127..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/generate-content.js +++ /dev/null @@ -1,128 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const marked = require('marked') - -const format = require('../../../../common/format') - -// generate titles and bodies from activity json data - -const pathPrefix = `/nonprofits/${app.nonprofit_id}` - -module.exports = exports = {} - -const viewPaymentLink = data => - h('p', [ h('a', {props: {href: `${pathPrefix}/payments?pid=${data.attachment_id}`}}, 'View payment details.') ]) - -exports.RecurringDonation = (data, state) => { - return { - title: `Paid $${format.centsToDollars(data.json_data.gross_amount)} towards a recurring donation` - , body: [ - viewDedication(data) - , h('p', `Started on ${format.date.toSimple(data.json_data.start_date)}. `) - , viewPaymentLink(data) - ] - , icon: 'fa-heart' - } -} - -const viewDedication = data => - data.json_data.dedication && data.json_data.dedication.name - ? h("p", [ - `Dedicated in ${data.json_data.dedication.type || 'honor'} of ` - , h('a', {props: {href: `/nonprofits/${ENV.nonprofitID}/supporters?sid=${data.json_data.dedication.supporter_id}`}}, data.json_data.dedication.name) - ]) - : '' - -exports.Donation = (data, state) => { - const desig = data.json_data.designation ? h('p', `Designation: ${data.json_data.designation}. `) : '' - return { - title: `Donated $${format.centsToDollars(data.json_data.gross_amount)}` - , body: [ - desig - , viewDedication(data) - , viewPaymentLink(data) - ] - , icon: 'fa-heart' - } -} - -exports.Ticket = (data, state) => { - var paren = data.json_data.gross_amount ? `(totalling $${format.centsToDollars(data.json_data.gross_amount)})` : '(for free)' - return { - title: `Redeemed ${data.json_data.quantity} tickets ${paren} for the event: ${data.json_data.event_name}` - , body: '' - , icon: 'fa-ticket' - } -} - -exports.Refund = (data, state) => { - return { - title: `Refunded $${format.centsToDollars(-data.json_data.gross_amount)}` - , body: [ - h('span', `Reason: ${format.snake_to_words(data.json_data.reason||'none')}. `) - , h('br') - , viewPaymentLink(data) - ] - , icon: 'fa-reply' - } -} - -exports.Dispute = (data, state) => { - return { - title: `This supporter disputed (made a charge-back) on their payment for $${format.centsToDollars(data.json_data.gross_amount)} on ${format.date.toSimple(data.json_data.original_date)}` - , body: [ - h('span', `Reason given: ${format.snake_to_words(data.json_data.reason||'none')}. `) - , h('br') - , viewPaymentLink(data) - ] - , icon: 'fa-ban' - } -} - -exports.SupporterNote = (data, state) => { - const action = data.created_at === data.updated_at ? 'added' : 'edited' - const canEdit = data.user_id === app.user_id - return { - title: `Note ${action}${data.json_data.user_email ? ' by ' + data.json_data.user_email : ''}` - , body: [ - h('span', {props: {innerHTML: marked(data.json_data.content ? data.json_data.content : '')}}) - , canEdit - ? h('span', [ - h('a.u-marginRight--10', {on: {click: [state.editNote$, data]}}, 'Edit ') - , h('span.u-color--red.u-pointer', {on: {click: [state.deleteNote$, data]}}, 'Delete') - ]) - : '' - ] - , icon: 'fa-pencil' - } -} - -exports.SupporterEmail = (data, state) => { - var jd = data.json_data - var canView = false - var body = [h('span', `Subject: ${jd.subject}`), h('br')] - var thread = h('a', {props: {href: '#'}, on: {click: [state.threadId$, jd.gmail_thread_id]}}, 'View thread') - - return { - title: `Email thread started by ${jd.from}` - , icon: 'fa-envelope' - , body: body - // , body: canView ? R.concat(body, thread) : R.concat(body, signIn) - } -} - -exports.OffsitePayment = (data, state) => { - const desig = data.json_data.designation ? `Designation: ${data.json_data.designation}. ` : '' - return { - title: `Donated $${format.centsToDollars(data.json_data.gross_amount)} (offsite)` - , body: [ - h('span', desig) - , desig ? h('br') : '' - , viewPaymentLink(data) - ] - , icon: 'fa-money' - } -} - - diff --git a/client/js/nonprofits/supporters/index/sidepanel/index.js b/client/js/nonprofits/supporters/index/sidepanel/index.js deleted file mode 100644 index 6dc50841..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/index.js +++ /dev/null @@ -1,147 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const url$ = require('flyd-url') -const render = require('ff-core/render') -const filter = require('flyd/module/filter') -const snabbdom = require('snabbdom') -const mergeAll = require('flyd/module/mergeall') -const sampleOn = require('flyd/module/sampleon') -const queryString = require('query-string') -const notification = require('ff-core/notification') - -const request = require('../../../../common/request') -const confirm = require('../../../../components/confirmation-modal') - -const actions = require('./supporter-actions') -const activities = require('./supporter-activities') -const offsiteDonationForm = require('./offsite-donation-form') -const supporterNoteForm = require('./supporter-note-form') - -const flatMap = R.curry(require('flyd/module/flatmap')) - -const init = _ => { - var state = { - clickComposing$: flyd.stream() - , threadId$: flyd.stream() - , newNote$: flyd.stream() - , editNote$: flyd.stream() - , deleteNote$: flyd.stream() - , newDonation$: flyd.stream() - } - - const supporterID$ = R.compose( - filter(Boolean ) - , flyd.map(url => queryString.parse(url.search).sid) - )(url$) - - state.pathPrefix$ = flyd.map(constructPathPrefix, supporterID$) - - const supporterPath$ = flyd.map(id => `/nonprofits/${app.nonprofit_id}/supporters/${id}`, supporterID$) - - const supporterResp$ = R.compose( - flyd.map(x => x.body.data) - , filter(x => x.status === 200) - , flatMap(path => request({method: 'get', path}).load) - )(supporterPath$) - - state.supporter$ = flyd.merge(supporterResp$, flyd.stream({})) - - - state.offsiteDonationForm = offsiteDonationForm.init(state) - - state.editNoteData$ = flyd.merge( - flyd.map(R.always({}), state.newNote$) - , flyd.map(d => ({id: d.attachment_id, content: d.json_data.content}), state.editNote$)) - - const deleteNoteId$ = flyd.map(d => d.attachment_id, state.deleteNote$) - - state.noteAjaxMethod$ = mergeAll([ - flyd.map(R.always('post'), state.newNote$) - , flyd.map(R.always('put'), state.editNote$) - ]) - - state.supporterNoteForm = supporterNoteForm.init(state) - - state.confirmDelete = confirm.init(deleteNoteId$) - - const deleteNoteResp$ = flatMap(ajaxDeleteNote(supporterPath$, deleteNoteId$), state.confirmDelete.confirm$) - - // All streams that we want to trigger a refresh of the supporter timeline - const fetchActivitiesWith$ = mergeAll([ - state.pathPrefix$ - , state.offsiteDonationForm.saved$ - , state.supporterNoteForm.saved$ - , deleteNoteResp$ - ]) - - // Stream of activities data, using the pathPrefix$ stream, triggered by fetchActivitiesWith$ - state.activities$ = R.compose( - R.curryN(2, flatMap)(getActivities) - , sampleOn(R.__, state.pathPrefix$) - )(fetchActivitiesWith$) - - state.activities = activities.init(state) - - state.modalID$ = mergeAll([ - , flyd.map(()=> 'newSupporterNoteModal', state.editNoteData$) - , flyd.map(()=> null, state.supporterNoteForm.saved$) - ]) - - - const message$ = mergeAll([ - , flyd.map(()=> 'Successfully created a new offsite contribution', state.offsiteDonationForm.saved$) - , flyd.map(()=> `Successfully ${noteMsg(state.noteAjaxMethod$)} supporter note`, state.supporterNoteForm.saved$) - , flyd.map(()=> 'Successfully deleted supporter note', deleteNoteResp$) - ]) - - state.notification = notification.init({message$}) - - window.state = state - return state -} - -const ajaxDeleteNote = (pathPrefix$, id$) => () => { - const path = `${pathPrefix$()}/supporter_notes/${id$()}` - return request({ - method: 'delete' - , path - }).load -} - -const noteMsg = method$ => { - if(method$() === 'put') return 'edited' - if(method$() === 'post') return 'created a new' -} - -const getActivities = path => - flyd.map(req => req.body, request({path: path + 'activities', method: 'get'}).load) - -const constructPathPrefix = sid => `/nonprofits/${app.nonprofit_id}/supporters/${sid}/` - -const view = state => { - return h('div', [ - actions.view(state) - , activities.view(state) - , notification.view(state.notification) - , offsiteDonationForm.view(R.merge(state.offsiteDonationForm)) - , supporterNoteForm.view(R.merge(state.supporterNoteForm, {modalID$: state.modalID$})) - , confirm.view(state.confirmDelete, 'Are you sure you want to delete this note?') - ]) -} - -var container = document.querySelector('#js-sidePanel') - -// -- Render to the page -// render takes state, view function, patch function, and DOM container -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/attributes') -, require('snabbdom/modules/style') -]) - -render({ patch, container , view, state: init() }) - diff --git a/client/js/nonprofits/supporters/index/sidepanel/offsite-donation-form.js b/client/js/nonprofits/supporters/index/sidepanel/offsite-donation-form.js deleted file mode 100644 index a75b722a..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/offsite-donation-form.js +++ /dev/null @@ -1,33 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const modal = require('ff-core/modal') -const button = require('ff-core/button') -const format = require('../../../../common/format') -const moment = require('moment') -const request = require('../../../../common/request') -const serialize = require('form-serialize') - -const flyd_flatMap = require('flyd/module/flatmap') -const flyd_mergeAll = require('flyd/module/mergeall') - -function init(parentState) { - var state = { - submit$: flyd.stream() - , supporter$: parentState.supporter$ - , saved$: flyd.stream() - } - - - return state -} - - -function view(state) { - - return h('div', {id$: 'offsite_donation_form_modal'}) -} - - -module.exports = {init, view} diff --git a/client/js/nonprofits/supporters/index/sidepanel/supporter-actions.js b/client/js/nonprofits/supporters/index/sidepanel/supporter-actions.js deleted file mode 100644 index 304d0ba3..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/supporter-actions.js +++ /dev/null @@ -1,25 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const flatMap = require('flyd/module/flatmap') -const flyd = require('flyd') -const h = require('snabbdom/h') -flyd.mergeAll = require('flyd/module/mergeall') - - -const button = (text, stream) => - h('button.button--tiny.u-marginRight--10', {on: {click: stream}} - , [h('i.fa.fa-plus.u-marginRight--5') , text ]) - -const view = state => - h('section.timeline-actions.u-padding--10', [ - button('Note', state.newNote$) - , button('Email', () => { window.open(`mailto:${state.supporter$().email}`)}) - , button('Donation', () => appl.open_donation_modal(state.supporter$().id, - () => {state.offsiteDonationForm.saved$(Math.random())} - ) - ) - ] - ) - -module.exports = {view} - diff --git a/client/js/nonprofits/supporters/index/sidepanel/supporter-activities.js b/client/js/nonprofits/supporters/index/sidepanel/supporter-activities.js deleted file mode 100644 index 5178eed8..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/supporter-activities.js +++ /dev/null @@ -1,74 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const moment = require('moment') -const flatMap = require('flyd/module/flatmap') -const request = require('../../../../common/request') -const flyd_mergeAll = require('flyd/module/mergeall') - -const generateContent = require('./generate-content') - -function init(parentState) { - const activitiesWithJson$ = flyd.map( - R.map(parseActivityJson) - , parentState.activities$ - ) - const response$ = flyd.merge( - flyd.stream([]) // default to empty array on pageload - , activitiesWithJson$ ) - const loading$ = flyd_mergeAll([ - flyd.map(() => false, response$) - ]) - return {response$, loading$} -} - -// Return js object if the string is json, otherwise return the string -const tryJSON = str => { - try { return JSON.parse(str) } catch(e) { return str } -} - -// Parse the cached `json_data` column for activities -// Also, parse the nested `dedicaton` json if it is present -const parseActivityJson = data => { - var json_data = JSON.parse(data.json_data || '{}') - json_data.dedication = tryJSON(json_data.dedication) - return R.merge(data, {json_data}) -} - -const view = parentState => { - var state = parentState.activities - if(state.loading$()) { - return h('div', [ - h('p.u-color--grey', [h('i.fa.fa-spin.fa-gear'), ' Loading timeline...']) - ]) - } - if(!state.loading$() && !state.response$().length) { - return h('div', [ - h('p.u-color--grey', 'No activity yet...') - ]) - } - return h('ul.timeline-activities', R.map(activityContent(parentState), state.response$())) -} - -// used to construct each activitiy list element -const activityContent = parentState => data => { - const contentFn = generateContent[data.kind] - if(!contentFn) return '' - const content = contentFn(data, parentState) - return h('li.timeline-activity', [ - h('div.timeline-activity-icon', [h(`i.fa.${content.icon}`)]) - , h('div.timeline-activity-card', [ - h('div', [ - h('small.u-color--grey', moment(data.date).format("ddd, MMMM Do YYYY")) - , h('div.u-fontSize--15', [ - h('strong', content.title) - , h('div', content.body) - ]) - ]) - ]) - ]) -} - -module.exports = {init, view} - diff --git a/client/js/nonprofits/supporters/index/sidepanel/supporter-note-form.js b/client/js/nonprofits/supporters/index/sidepanel/supporter-note-form.js deleted file mode 100644 index fa786130..00000000 --- a/client/js/nonprofits/supporters/index/sidepanel/supporter-note-form.js +++ /dev/null @@ -1,95 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const modal = require('ff-core/modal') -const button = require('ff-core/button') -const request = require('../../../../common/request') -const sampleOn = require('flyd/module/sampleon') -const serialize = require('form-serialize') -const flyd_filter = require('flyd/module/filter') -const flyd_flatMap = require('flyd/module/flatmap') -const flyd_mergeAll = require('flyd/module/mergeall') - -function init(parentState) { - var state = { - submit$: flyd.stream() - , supporter$: parentState.supporter$ - , editData$: flyd.merge(flyd.stream({}), parentState.editNoteData$) - , ajaxMethod$: parentState.noteAjaxMethod$ - } - - const sendData$ = flyd.map(formatData(state), state.submit$) - - const resp$ = flyd_flatMap(d => request(d).load, sendData$) - - state.saved$ = flyd_filter(req => req.status === 200, resp$) - - state.error$ = flyd_mergeAll([ - flyd.map(()=> null, state.submit$) - , flyd.map(req => 'Sorry! There was an error. Please try again soon.', flyd_filter(req => req.status !== 200, resp$)) - ]) - - const resetForm$ = sampleOn(state.saved$, state.submit$) - - flyd.map(x => x.reset(), resetForm$) - - state.loading$ = flyd_mergeAll([ - flyd.map(()=> true, state.submit$) - , flyd.map(() => false, resp$) - ]) - return state -} - -const formatData = state => form => { - form = serialize(form, {hash: true}) - const path = `/nonprofits/${app.nonprofit_id}/supporters/${form.supporter_id}/supporter_notes` - const id = state.editData$().id - return { - method: state.ajaxMethod$() - , path: id ? `${path}/${id}` : path - , send: {supporter_note: form} - } -} - -function view(state) { - var body = form(state) - return h('div', [ - modal({ - id$: state.modalID$ - , thisID: 'newSupporterNoteModal' - , title: (state.editData$().content ? 'Edit' : 'New') + ' Supporter Note' - , body - }) - ]) -} - -const form = state => { - return h('form', { - on: {submit: ev => {ev.preventDefault(); state.submit$(ev.currentTarget)}} - }, [ - h('p', ['You can use ', h('a', {props: {href: 'https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet', target: '_blank'}}, 'Markdown'), ' here.']) - , h('input', { - props: { - type: 'hidden' - , name: 'supporter_id' - , value: state.supporter$().id - } - }) - , h('fieldset', [ - h('textarea', { - props: { - rows: 3 - , name: 'content' - , placeholder: 'Write your note here for this supporter.' - , value: state.editData$().content || '' - } - }) - ]) - , h('div.u-centered', [ - button({loading$: state.loading$, error$: state.error$}) - ]) - ]) -} - -module.exports = {init, view} diff --git a/client/js/nonprofits/supporters/index/supporter_details.js b/client/js/nonprofits/supporters/index/supporter_details.js deleted file mode 100644 index bbb2d84d..00000000 --- a/client/js/nonprofits/supporters/index/supporter_details.js +++ /dev/null @@ -1,183 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/super-agent-promise') -var format_err = require('../../../common/format_response_error') -var create_offline_donation = require('../../../donations/create_offline') - -appl.def('supporter_details', { - resource_name: 'supporters', - - // Assign the selected supporter to the one clicked on - // Fetch the full data for that supporter with ajax after setting - // Show the side panel - show: function(supporter) { - appl.open_side_panel() - appl.def('supporter_details', supporter) - appl.def('supporters.selected', [supporter]) - appl.def('timeline_action', null) - appl.ajax_supporter.fetch(supporter.id) - var path = 'supporters/' + supporter.id + '/' - appl.def('supporter_details.tags.path_prefix', path) - appl.def('supporter_details.activities.path_prefix', path) - appl.def('supporter_details.supporter_notes.path_prefix', path) - appl.def('supporter_details.custom_fields.path_prefix', path) - request.get('/nonprofits/' + app.nonprofit_id + '/supporters/' + supporter.id + '/tag_joins').perform() - .then(function(r) { appl.def('supporter_details.tags', r.body) }) - appl.ajax.index('supporter_details.custom_fields') - }, - - toggle_panel: function(supporter, node) { - appl.close_modal() - var tr = node.parentNode.parentNode - - if(tr.hasAttribute('data-selected')) { - appl.close_side_panel() - tr.removeAttribute('data-selected','') - } else { - appl.supporter_details.show(supporter) - $('.mainPanel').find('tr').removeAttr('data-selected') - tr.setAttribute('data-selected','') - // add supporter_id to url param - var path = window.location.pathname + "?sid=" + supporter.id - window.history.pushState({},'supporter id', path) - } - } -}) - - -appl.def('ajax_supporter', { - update: function(id, form_obj, node) { - appl.def('loading', true) - appl.ajax.update('supporter_details', id, form_obj).then(function(resp) { - appl.def('loading', false) - appl.supporters.index() - if(resp.body.deleted) { - appl.find_and_remove('supporters.data', {id: resp.body.id}) - appl.close_side_panel() - appl.notify('Supporter successfully deleted') - } else { - appl.find_and_set('supporters.data', {id: resp.id}, resp) - appl.close_modal() - appl.notify('Supporter updated!') - } - }) - }, - - fetch: function(id) { - appl.def('loading', true) - appl.ajax.fetch('supporter_details', id).then(function(resp) { - appl.def('supporter_details.data.name_email_or_id', - resp.body.data.name || resp.body.data.fc_full_name || resp.body.data.email || 'Supporter #' + resp.body.data.id) - appl.def('supporter_details.data.websites', - resp.body.data.fc_websites ? resp.body.data.fc_websites.split(',') : false - ) - appl.def('loading', false) - }) - fetch_full_contact(id) - } -}) - -function fetch_full_contact(id){ - appl.def('supporter_details.data.full_contact', {photo: false, current_job: false, interests: false, jobs: false, social: false}) - request.get('/nonprofits/' + app.nonprofit_id + '/supporters/' + id + '/full_contact').perform() - .then(function(resp){ - var data = resp.body.full_contact - if (!data) { - appl.def('supporter_details.data.full_contact', false) - return - } - appl.def('supporter_details.data.full_contact', { - photo : data.photo && data.photo[0] ? data.photo[0].url : false - , current_job : data.orgs ? data.orgs.map(function(d){if (d.current) return d })[0] : false - , interests : data.topics - , jobs: data.orgs - , social: data.profiles - }) - }) -} - - -appl.ajax_supporter.create = function(form_obj, node) { - appl.def('supporter_details', {loading: true, error: ''}) - return request.post('/nonprofits/' + app.nonprofit_id + '/supporters').send({supporter: form_obj}).perform() - .then(function() { - appl.def('supporter_details', {loading: false}) - appl.close_modal() - appl.notify("Supporter successfully created!") - appl.supporters.index() - appl.prev_elem(node).reset() - }) - .catch(function(resp) { - appl.def('supporter_details', {error: format_err(resp), loading: false}) - }) -} - - -appl.def('supporter_details.tags', { - resource_name: 'tag_joins' -}) - -appl.def('supporter_details.custom_fields', { - resource_name: 'custom_field_joins' -}) - - -appl.def('supporter_details.activities', { - resource_name: 'activities' -}) - -appl.def('supporter_details.supporter_notes', { - resource_name: 'supporter_notes' -}) - - -// Override the default 'close_side_panel' function provided by -// panels_layout.js so we can set some extra data -var old_close_fn = appl.close_side_panel -appl.def('close_side_panel', function(){ - appl.def('supporters.selected', appl.get_checked_supporters()) - old_close_fn.apply(appl) -}) - - - -appl.def('delete_selected_supporters', function(id){ - appl.supporters.selected.forEach(function(supp) { - appl.ajax_supporter.update(supp.id, {deleted: true}) - }) - appl.close_side_panel() -}) - -appl.def('supporter_details.address_with_commas', utils.address_with_commas) - - -appl.def('create_offline_donation', function(form_obj, el) { - create_offline_donation(form_obj, createDonationUI) - .then(function(resp) { - appl.ajax.index('supporter_details.activities') - appl.prev_elem(el).reset() - }) -}) - -var createDonationUI = { - start: function(){ - appl.is_loading() - appl.def('new_offline_donation', {loading: true, error: ''}) - }, - success: function(){ - appl.not_loading() - appl.def('new_offline_donation', {loading: false}) - appl.notify("Offline donation created successfully") - appl.close_modal() - }, - fail: function(resp){ - appl.def('new_offline_donation', {loading: false, error: format_err(resp)}) - appl.def('loading', false).def('error', format_err(resp)) - } -} - -// Initialize the date picker inside the offline donation modal -var Pikaday = require('pikaday') -new Pikaday({ - field: document.querySelector('#js-offsiteDonationDate'), - format: 'M/D/YYYY' -}) diff --git a/client/js/nonprofits/supporters/index/tags_and_fields_shared_methods.js b/client/js/nonprofits/supporters/index/tags_and_fields_shared_methods.js deleted file mode 100644 index ece41743..00000000 --- a/client/js/nonprofits/supporters/index/tags_and_fields_shared_methods.js +++ /dev/null @@ -1,59 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../../common/client') -var endpoint_prefix = '/nonprofits/' + app.nonprofit_id + '/' -var tags_or_fields = {} - - -tags_or_fields.master_endpoint = function(type){ - return endpoint_prefix + type + '_masters' -} - - -tags_or_fields.index_masters = function(type) { - request.get(tags_or_fields.master_endpoint(type)) - .end(function(err, resp) { - appl.def(type + 's.masters.data', appl.sort_arr_of_objs_by_key(resp.body.data, 'name')) - }) -} - - -tags_or_fields.add = function(obj){ - if(obj.form_obj.name === '') { - appl.notify('Sorry, input cannot be blank') - return - } - var loading_key ='manage_' + obj.type + 's.loading' - var data_key = obj.type + '_master' - var endpoint = endpoint_prefix + data_key + 's' - var data = {}; data[data_key] = obj.form_obj - var notify_text = (obj.type === 'tag' ? 'Tag' : 'Field') + ' "' + obj.form_obj.name + '"' - - appl.def(loading_key, true) - - request.post(endpoint, data) - .end(function(err, resp) { - tags_or_fields.index_masters(obj.type) - appl.prev_elem(obj.node).reset() - appl.def(loading_key, false) - if (resp.text === '["Duplicate tag"]') - appl.notify(notify_text + ' already exists.') - else if (resp.status != 200 ) - appl.notify('Sorry, could not process request') - else - appl.notify(notify_text + ' successfully added.') - }) -} - - -tags_or_fields.delete = function(obj){ - var notify_type = (obj.type === 'tag' ? 'Tag ' : 'Field ') - request.del(tags_or_fields.master_endpoint(obj.type) + '/' + obj.id) - .end(function(err, resp) { - tags_or_fields.index_masters(obj.type) - appl.notify(notify_type + '"' + obj.name + '" successfully deleted.') - if(obj.cb) obj.cb() - }) -} - - -module.exports = tags_or_fields \ No newline at end of file diff --git a/client/js/nonprofits/supporters/index/timeline.js b/client/js/nonprofits/supporters/index/timeline.js deleted file mode 100644 index 8f43e9eb..00000000 --- a/client/js/nonprofits/supporters/index/timeline.js +++ /dev/null @@ -1,69 +0,0 @@ -// License: LGPL-3.0-or-later - -appl.def('timeline.make_clickable', function(node){ - var card = appl.prev_elem(node) - card.setAttribute('clickable', '') -}) - -appl.def('timeline.show_email', function(email, date) { - email.date = date - set_readonly_email(email) - appl.open_modal('emailReadOnlyModal') -}) - -appl.def('timeline.show_note', function(note, date) { - appl.def('current_note', { - date: date, - content: note.content, - id: note.id, - is_editing: false - }) - appl.open_modal('noteModal') -}) - -function set_readonly_email(email) { - appl.def('timeline.displaying_email', { - body: email.body.replace(/{{NAME}}/g, appl.supporter_details.data.name_email_or_id), - subject: email.subject, - date: email.date - }) -} - - -appl.def('ajax_supporter_notes', { - create: function(form_obj, node) { - appl.is_loading() - appl.ajax.create('supporter_details.supporter_notes', form_obj).then(function(resp) { - appl.not_loading() - if(!resp.ok) return appl.notify("Sorry! Unable to post note: " + resp.body) - appl.def('timeline_action', null) - appl.ajax.index('supporter_details.activities') - appl.notify("Note added") - node.parentNode.reset() - }) - }, - update: function(form_obj) { - appl.is_loading() - appl.ajax.update('supporter_details.supporter_notes', form_obj['id'], form_obj).then(function(resp) { - appl.not_loading() - if(!resp.ok) return appl.notify("Sorry! Unable to update note: " + resp.body) - appl.ajax.index('supporter_details.activities') - appl.notify("Note updated") - }) - }, - delete: function(id) { - appl.is_loading() - appl.close_modal() - appl.ajax.del('supporter_details.supporter_notes', id).then(function(resp) { - appl.not_loading() - if(!resp.ok) return appl.notify("Sorry! Unable to delete note: " + resp.body) - appl.ajax.index('supporter_details.activities') - appl.notify("Note deleted") - }) - } -}) - -appl.def('get_donation_url', function(donation) { - var search_id = (donation && donation.payment && donation.payment.id) ? ('?pid=' + donation.payment.id) : ('?sid=' + appl.supporter_details.id) - return "/nonprofits/" + app.nonprofit_id + "/payments" + search_id -}) diff --git a/client/js/nonprofits/supporters/index/tour.js b/client/js/nonprofits/supporters/index/tour.js deleted file mode 100644 index b1a02e2c..00000000 --- a/client/js/nonprofits/supporters/index/tour.js +++ /dev/null @@ -1,86 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../../common/vendor/bootstrap-tour-standalone') - -var supporters_tour = new Tour({ - backdrop: false, - steps: [ - { - orphan: true, - title: 'Welcome to your supporters dashboard!', - content: "This page is the hub for all of your supporter data and supporter related actions, such as emailing, tagging, merging, adding notes and more. You'll notice that there is not much data here yet. Click 'Next' to find out how to import supporter data." - }, - { - orphan: true, - title: 'Importing supporter data 1/3', - content: "There are three ways that supporter data can be added to the CRM. The first method is automatic - whenever anyone makes a donation, contributes to a campaign or buys a ticket to an event, their information is automatically added here. That means no more tedious data entry for you or your team." - }, - { - element: '.tour-addSupporter', - placement: 'left', - title: 'Importing supporter data 2/3', - content: "The second method is manually - you can simply add a supporter by clicking on this button and adding the new supporter's info into a form." - }, - { - element: '.tour-import', - placement: 'left', - title: 'Importing supporter data 3/3', - content: "The third method is to import your supporter data. You can click this button and we'll walk you through the import process. " - }, - { - element: '.tour-supporters', - placement: 'top', - title: 'Supporter profile 1/2', - content: "Clicking on a supporter's row will open up that supporter's panel. The supporter panel shows that supporter's details, a timeline of their activities and actions." - }, - { - element: '.sidePanel', - placement: 'left', - title: 'Supporter profile 2/2', - onShow: openSidePanel, - onHide: appl.close_side_panel, - content: "From the supporter panel, you can edit the supporter's fields, add notes, tag, send an email or add an offline donation. All activity tied to the supporter, such as sending an email or attending an event, will automatically be added to their timeline. In addition, if the supporter has any publicly available social media data, we will add it to this panel." - }, - { - element: '.tour-bulk', - placement: 'bottom', - title: 'Bulk actions', - onShow: showBulkActions, - onHide: hideBulkActions, - content: "To access your bulk actions, just click on the supporters' checkboxes that you want to perform the bulk action on. To perform a bulk action on all of your supporters, click the checkbox on the top left corner." - }, - { - orphan: true, - title: 'Need more help?', - content: "There are still more features in our CRM that we weren't able to cover on this tour, such as creating email templates and adding donate buttons to your supporter emails. If you want a walk-through of any features or have any questions or comments, please email support@commitchange.com. We're here to help." - } - ] -}) - -if($.cookie('tour_supporters') === String(app.nonprofit_id)) { - $.removeCookie('tour_supporters', {path: '/'}) - supporters_tour.init() - supporters_tour.restart() -} - - -function openSidePanel(){ - if(!appl.supporters.data){return} - appl.def('supporter_details.data', appl.supporters.data[0]) - appl.open_side_panel() -} - -function showBulkActions(){ - if(!appl.supporters.data){return} - appl.def('supporters.data', set_checked(appl.supporters.data, true)) - appl.def('supporters.selected', appl.supporters.data) -} - -function hideBulkActions(){ - appl.def('supporters.data', set_checked(appl.supporters.data, false)) - appl.def('supporters.selected', '') -} - -function set_checked(supporters, state) { - return supporters.map(function(s) {s.is_checked = state; return s}) -} - diff --git a/client/js/nonprofits/supporters/new/page.js b/client/js/nonprofits/supporters/new/page.js deleted file mode 100644 index b7ec2328..00000000 --- a/client/js/nonprofits/supporters/new/page.js +++ /dev/null @@ -1,24 +0,0 @@ -// License: LGPL-3.0-or-later -var restful_resource = require('../../../common/restful_resource') - -appl.def('supporter', { - path_prefix: '/nonprofits/' + app.nonprofit_id + '/', - resource_name: 'supporters', - after_create_failure: function(resp) { - appl.def('error', resp).def('loading', false) - }, - before_create: function(obj) { - obj.tags_attributes = [{ - parent_id: app.nonprofit_id, - parent_type: 'Nonprofit', - name: 'volunteer' - }] - appl.def('error', '').def('loading', true) - }, - after_create: function(resp, node){ - appl.def('loading', false) - appl.notify("Volunteer created!") - appl.redirect('/nonprofits/' + app.nonprofit_id) - } -}) - diff --git a/client/js/page.js b/client/js/page.js deleted file mode 100644 index 080ee951..00000000 --- a/client/js/page.js +++ /dev/null @@ -1,75 +0,0 @@ -// License: LGPL-3.0-or-later -// vendor -window.utils = require('./common/utilities') // XXX remove -window.appl = require('./common/application_view') // XXX remove - -window.$ = require('jquery') // XXX remove -window.jQuery = window.$ // XXX remove -require('./common/polyfills') -require('./common/vendor/jquery.cookie') // XXX remove -require('parsleyjs') // XXX remove -require('./common/jquery_additions') // XXX remove -require('./common/autosubmit') // XXX remove - -// Application-wide concerns - -// Use the proper CSRF token on every ajax request using jQuery. -// XXX remove -$.ajaxSetup({ headers: { 'X-CSRF-Token': window._csrf } }) -appl.def('csrf', window._csrf) - -// The 'notice' cookie is used for one-time messages (just like flash[:notice] in the session) -// XXX remove -if ($.cookie('notice') || $.cookie('notice') === '') { - $.removeCookie('notice', {path: '/'}) -} if ($.cookie('error') || $.cookie('error') === '') { - $.removeCookie('error', {path: '/'}) -} - -// Input clear button -- put after the input -// XXX remove -$('.clear-input').click(function(e) { - $(this).prev().val('').trigger('change') -}) - - -// XXX remove -$('*[open-modal]').click(function(e) { - e.preventDefault() - var el = e.currentTarget - $('.modal').removeClass('inView') - $('body').addClass('is-showingModal') - - if((el.hasAttribute('data-when-confirmed') || el.hasAttribute('data-when-signed-in')) && !app.user) - $('#signUpModal').addClass('inView') - else if(el.hasAttribute('data-when-confirmed') && app.user && !app.user.confirmed) - $('#emailConfirmationModal').addClass('inView') - else - $('#' + this.getAttribute('open-modal')).addClass('inView') -}) - -// XXX remove -$('body').on('click', '.modal-backdrop', function() { - $('body').removeClass('is-showingModal') - $('.modal').removeClass('inView') -}) - -// XXX remove -$("*[tooltip]").each(function() { $(this).tooltip() }) - -// XXX remove -$('.sortArrows').click(function() { - var $sortArrows = $(this) - var sort = $sortArrows.attr('sort') - if (sort === 'desc') $sortArrows.attr('sort', 'asc') - else if (sort === 'asc') $sortArrows.attr('sort', 'none') - else $sortArrows.attr('sort', 'desc') -}) - -// Hide server-side flash notice message after 7s -const flash = document.querySelector('.flash') -if(flash) { - setTimeout(function() { - flash.className = flash.className + ' u-hide' - }, 7000) -} diff --git a/client/js/pages/show/index.js b/client/js/pages/show/index.js deleted file mode 100644 index 527cc1ed..00000000 --- a/client/js/pages/show/index.js +++ /dev/null @@ -1,5 +0,0 @@ -// License: LGPL-3.0-or-later -var editable = require('../../common/editable') - -if(app.current_admin) - editable($('.editable'), {sticky: true}) diff --git a/client/js/recurring_donations/edit/amount-step.es6 b/client/js/recurring_donations/edit/amount-step.es6 deleted file mode 100644 index c6596d96..00000000 --- a/client/js/recurring_donations/edit/amount-step.es6 +++ /dev/null @@ -1,111 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const format = require('../../common/format') -flyd.scanMerge = require('flyd/module/scanmerge') - -function init(donationDefaults, params$) { - var state = { - params$: params$ - , evolveDonation$: flyd.stream() // Stream of objects that can be used to R.evolve the initial donation object - , buttonAmountSelected$: flyd.stream(true) // Whether the button or input is selected - , currentStep$: flyd.stream() - } - - // A stream of objects that an be used to modify the existing donation by using R.evolve - donationDefaults = R.merge(donationDefaults, { - amount: format.dollarsToCents(state.params$().single_amount || 0) - , recurring: state.params$().type === 'recurring' - }) - // Apply R.evolve using every value on the evolveDonation$ stream, starting with the defaults - state.donation$ = flyd.scanMerge([ - [state.params$ || flyd.stream(), setDonationFromParams] - , [state.evolveDonation$, R.flip(R.evolve)] - ], donationDefaults) - - return state -} - -const setDonationFromParams = (donation, params) => { - if(params.single_amount) donation.amount = format.dollarsToCents(params.single_amount) - if(params.designation) donation.designation = params.designation - donation.recurring = params.type === 'recurring' - return donation -} - -function view(state) { - const isRecurring = state.donation$().recurring - return h('div.wizard-step.amount-step', [ - chooseNewDonationAmount() - , amountFields(state) - ]) -} - -// If recurring, an extra message to reinforce that it is in fact charged every month -function recurringMessage(isRecurring, state) { - if(!isRecurring) return '' - return h('section.donate-recurringMessage.group', [ - h('p.u-paddingX--5.u-centered', { - class: {'u-hide': !isRecurring} - }, [ - state.params$().single_amount ? '' : h('small', [ - 'Select an amount for your ' - , h('strong', 'monthly') - , ' contribution' - ]) - ]) - ]) -} - - -function chooseNewDonationAmount() { - - return h('section.donate-recurringMessage.group', [ - h('p.u-paddingX--5.u-centered', 'Choose your new donation amount') - ]) -} - -// All the buttons and the custom input for the amounts to select -function amountFields(state) { - if(state.params$().single_amount) return '' - return h('div.u-inline.fieldsetLayout--three--evenPadding', [ - h('span', - R.map( - amt => h('fieldset', [ - h('button.button.u-width--full.white.amount', { - class: {'is-selected': state.buttonAmountSelected$() && state.donation$().amount === amt*100} - , on: {click: ev => { - state.evolveDonation$({amount: R.always(format.dollarsToCents(amt))}) - state.buttonAmountSelected$(true) - state.currentStep$(1) // immediately advance steps when selecting an amount button - } } - }, [ - h('span.dollar', '$') - , String(amt) - ]) - ]) - , state.params$().custom_amounts || [] ) - ) - , h('fieldset.prepend--dollar', [ - h('input.amount', { - props: {name: 'amount', step: 'any', type: 'number', min: 1, placeholder: 'Custom'} - , class: {'is-selected': !state.buttonAmountSelected$()} - , on: { - focus: ev => state.buttonAmountSelected$(false) - , change: ev => state.evolveDonation$({amount: R.always(format.dollarsToCents(ev.currentTarget.value))}) - } - }) - ]) - , h('fieldset', [ - h('button.button.u-width--full', { - props: {type: 'submit', disabled: !state.donation$().amount || state.donation$().amount <= 0} - , on: {click: [state.currentStep$, 1]} - }, 'Next') - ]) - ]) -} - - - -module.exports = {view, init} \ No newline at end of file diff --git a/client/js/recurring_donations/edit/branded-wizard.es6 b/client/js/recurring_donations/edit/branded-wizard.es6 deleted file mode 100644 index 6b661988..00000000 --- a/client/js/recurring_donations/edit/branded-wizard.es6 +++ /dev/null @@ -1,51 +0,0 @@ -// License: LGPL-3.0-or-later - -const gradient = require('../../common/css-gradient') -const customBranding = require('./custom-nonprofit-branding.es6') - -const bg = color => `background-color: ${color} !important;` - -module.exports = function (brand_color = null) { - var colors = customBranding(brand_color) - - return ` -.wizard-steps div.is-selected, -.wizard-steps button.is-selected { - ${bg(colors.lighter)} -} -.wizard-steps .button.white { - color: #494949; -} -.wizard-steps a:not(.button--small), -.ff-wizard-index-label.ff-wizard-index-label--accessible, -.wizard-index-label.is-accessible { - color: ${colors.dark} !important; -} -.wizard-steps input.is-selected { - border-color: ${colors.light} !important; -} -.wizard-steps button:not(.white):not([disabled]) { - ${bg(colors.dark)} -} -.wizard-steps .highlight { - ${bg(colors.lightest)} -} -.wizard-steps label, -.wizard-steps th { - color: #636363; -} - -.wizard-steps input[type='radio']:checked + label:before { - ${bg(colors.base)} -} - -.wizard-steps input[type='checkbox'] + label:before { - color: ${colors.base} !important; -} - -.ff-wizard-index-label.ff-wizard-index-label--current, -.wizard-index-label.is-current { - ${gradient('left', '#fbfbfb', colors.light)} -} -` -} diff --git a/client/js/recurring_donations/edit/card-form.es6 b/client/js/recurring_donations/edit/card-form.es6 deleted file mode 100644 index 4e89742e..00000000 --- a/client/js/recurring_donations/edit/card-form.es6 +++ /dev/null @@ -1,186 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const h = require('snabbdom/h') -const R = require('ramda') -const validatedForm = require('ff-core/validated-form') -const button = require('ff-core/button') -const flyd = require('flyd') -flyd.flatMap = require('flyd/module/flatmap') -flyd.filter = require('flyd/module/filter') -flyd.mergeAll = require('flyd/module/mergeall') -const scanMerge = require('flyd/module/scanmerge') -// local -const request = require('../../common/request') -const formatErr = require('../../common/format_response_error') -const createCardStream = require('../../cards/create-frp.es6') -const serializeForm = require('form-serialize') -const luhnCheck = require('../../common/credit-card-validator.js') - -// A component for filling out card data, validating it, saving the card to -// stripe, and then saving a tokenized copy to our servers. - -// Form validation constraints, validator functions, and error messages: -var constraints = { - address_zip: {required: true} -, name: {required: true} -, number: {required: true, cardNumber: true} -, exp_month: {required: true, format: /\d\d?/} -, exp_year: {required: true, format: /\d\d?/} -, cvc: {required: true, format: /\d\d\d\d?/} -} -var validators = { cardNumber: luhnCheck } -var messages = { - number: { - required: "Please enter your card number" - , cardNumber: "That card number doesn't look right" - } -} - -// You can pass in the .hideButton boolean if you want to control whether the submit button is shown/hidden -// Pass in a .card object, which can have default objects for the card form (name, number, cvc, exp_month, etc) -// Pass in .path to set the endpoint for saving the card -// Pass in .payload for default data to send to the server for every card save request (such as a request token) -const init = (state) => { - state = state || {} - // set defaults - state = R.merge({ - payload$: flyd.stream(state.payload || {}) - , path$: flyd.stream(state.path || '/cards') - , outerError$: state.error$ || flyd.stream() - }, state) - - state.form = validatedForm.init({constraints, validators, messages}) - state.card$ = flyd.merge(flyd.stream(state.card || {}), state.form.validData$) - - // streams of stripe tokenization responses - const stripeResp$ = flyd.flatMap(createCardStream, state.form.validData$) - state.stripeRespOk$ = flyd.filter(r => !r.error, stripeResp$) - const stripeError$ = flyd.map(r => r.error.message, flyd.filter(r => r.error, stripeResp$)) - - // Save the card as a card table on our own db - // streams of responses - state.resp$ = flyd.flatMap( - resp => saveCard(state.payload$(), state.path$(), resp) // cheating on the streams here.. - , state.stripeRespOk$ ) - - const ccError$ = flyd.map(R.prop('error'), flyd.filter(resp => resp.error, state.resp$)) - state.saved$ = flyd.filter(resp => !resp.error, state.resp$) - state.error$ = flyd.merge(stripeError$, ccError$,state.outerError$) - - state.loading$ = scanMerge([ - [state.form.validSubmit$, R.always(true)] - , [state.error$, R.always(false)] - , [state.saved$, R.always(false)] - ], false) - - return state -} - - -// -- Stream-related functions - -// Save the card to our own servers, and return a response stream -const saveCard = (send, path, resp) => { - send.card = R.merge(send.card, { - cardholders_name: resp.name - , name: `${resp.card.brand} *${resp.card.last4}` - , stripe_card_token: resp.id - , stripe_card_id: resp.card.id - }) - return flyd.map(R.prop('body'), request({ path, send, method: 'post' }).load) -} - - -// -- Virtual DOM - -const view = state => { - var field = validatedForm.field(state.form) - return validatedForm.form(state.form, h('form.cardForm', [ - h('div.u-background--grey.group.u-padding--8', [ - nameInput(field, state.card$().name) - , numberInput(field) - , cvcInput(field) - , expMonthInput(field) - , expYearInput(field) - , zipInput(field, state.card$().address_zip) - , profileInput(field, app.profile_id) // XXX global - ]) - , h('div.u-centered.u-marginTop--20', [ - state.hideButton ? '' : button({ - error$: state.hideErrors ? flyd.stream() : state.error$ - , loading$: state.loading$ - }) - , h('p.u-fontSize--12.u-marginBottom--0.u-marginTop--10.u-color--grey', [ h('i.fa.fa-lock'), " Transactions secured with 256-bit SSL"]) - ]) - ]) ) -} - - -const nameInput = (field, name) => - h('fieldset', [ field(h('input', { props: { name: 'name' , value: name || '', placeholder: "Cardholder's Name" } })) ]) - - -const numberInput = field => - h('fieldset.col-8', [ field(h('input', {props: { type: 'text' , name: 'number' , placeholder: 'Card Number' } })) ]) - - -const cvcInput = field => - h('fieldset.col-right-4.u-relative', [ - field(h('input', { props: { name: 'cvc' , placeholder: 'CVC' } } )) - , h('img.security-code-image', { - src: `${app.asset_path}/graphics/cc-security-code.png` - }) - ]) - - -const expMonthInput = field => { - var options = R.prepend( - h('option.default', {props: {value: undefined, selected: true}}, 'Month') - , R.range(1, 13).map(n => h('option', String(n))) - ) - return h('fieldset.col-4.u-margin--0', [ - field(h('select.select' - , { props: {name: 'exp_month'} } - , options)) - ]) -} - - -const expYearInput = field => { - var yearRange = R.range(new Date().getFullYear(), new Date().getFullYear() + 15) - var options = R.prepend( - h('option.default', {props: {value: undefined, selected: true}}, 'Year') - , R.map(y => h('option', String(y)), yearRange) - ) - return h('fieldset.col-left-4.u-margin--0', [ - field(h('select.select' - , {props: {name: 'exp_year'}} - , options)) - ]) -} - - -const zipInput = (field, zip) => - h('fieldset.col-right-4.u-margin--0', [ - field(h('input' - , { props: { - type: 'text' - , name: 'address_zip' - , value: zip || '' - , placeholder: 'Zip Code' - }} - )) - ]) - - -const profileInput = (field, profile_id) => - field(h('input' - , { props: { - type: 'hidden' - , name: 'profile_id' - , value: profile_id || '' - }} - )) - -module.exports = {view, init} - diff --git a/client/js/recurring_donations/edit/change-amount-wizard.es6 b/client/js/recurring_donations/edit/change-amount-wizard.es6 deleted file mode 100644 index 322f6c8b..00000000 --- a/client/js/recurring_donations/edit/change-amount-wizard.es6 +++ /dev/null @@ -1,132 +0,0 @@ -// License: LGPL-3.0-or-later -const flyd = require('flyd') -const R = require('ramda') -const h = require('snabbdom/h') -const url = require('url') -const render = require('ff-core/render') -const wizard = require('ff-core/wizard') -const scanMerge = require('flyd/module/scanmerge') -flyd.mergeAll = require('flyd/module/mergeall') -flyd.flatMap = require('flyd/module/flatmap') -flyd.zip = require('flyd-zip') - -const getParams = require('./get-params') - -const paymentStep = require('./payment-step.es6') -const amountStep = require('./amount-step.es6') -const followupStep = require('./followup-step') - -const request = require('../../common/request') -const format = require('../../common/format') - -const brandedWizard = require('./branded-wizard.es6') -const renderStyles = require('../../components/styles/render-styles') - - - -// pass in a stream of configuration parameters -const init = params => { - var state = { - error$: flyd.stream() - , loading$: flyd.stream() - , clickFinish$: flyd.stream() - , params$: flyd.map(getParams, flyd.stream(params)) - } - - renderStyles()(brandedWizard(state.params$().nonprofit.brand_color ? state.params$().nonprofit.brand_color : null)) - - app.campaign = app.campaign || {} // so we don't have to hot switch all the calls to app.campaign.name, etc - var donationDefaults = setDonationFromParams({ - nonprofit_id: app.nonprofit_id - , campaign_id: app.campaign.id - , event_id: app.event_id - }, state.params$()) - - state.amountStep = amountStep.init(donationDefaults, state.params$) - - state.donation$ = scanMerge([ - [state.amountStep.donation$, R.merge] - - , [state.params$, setDonationFromParams] - - ], donationDefaults ) - - state.paymentStep = paymentStep.init(state.params$, state.donation$) - - const currentStep$ = flyd.mergeAll([ - state.amountStep.currentStep$ - , flyd.map(R.always(0), state.params$) // if the params ever change, jump back to step one - , flyd.stream(0) - ]) - state.wizard = wizard.init({currentStep$, isCompleted$: state.paymentStep.success$}) - - - // Handle the Finish button from the followup step -- will close modal, redirect, or refresh - flyd.lift( - (ev, params) => { - if(!parent) return - if(params.redirect) parent.postMessage(`commitchange:redirect:${params.redirect}`, '*') - else if(params.mode !== 'embedded') parent.postMessage('commitchange:close', '*') - } - , state.clickFinish$, state.params$ ) - - return state -} - -const setDonationFromParams = (don, params) => { - if(!params.single_amount || isNaN(format.dollarsToCents(params.single_amount))) delete params.single_amount - return R.merge({ - amount: params.single_amount ? format.dollarsToCents(params.single_amount) : 0 - }, don) -} - - -const view = state => { - return h('div', { - // class: {'is-modal': state.params$().offsite} - }, [ - // h('img.closeButton', { - // props: {src: '/assets/ui_components/close.svg'} - // , on: {click: ev => state.params$().offsite ? parent.postMessage('commitchange:close', '*') : null} - // , class: {'u-hide': !state.params$().offsite} - // }) - h('div.titleRow', [ - h('img', {props: {src: app.pageLoadData.nonprofit.logo.normal.url}}) - , h('div.titleRow-info', [ - h('h2', app.pageLoadData.nonprofit.name ) - , h('p', [ - state.params$().designation && !state.params$().single_amount - ? headerDesignation(state) - : app.pageLoadData.nonprofit.tagline || '' - ]) - ]) - ]) - , wizardWrapper(state) - ]) -} - -const headerDesignation = state => { - return h('span', [ - h('i.fa.fa-star', {style: {color: app.nonprofit.brand_color || ''}}) - , h('strong', ' Designation: ') - , String(state.params$().designation) - , state.params$().designation_desc - ? h('span', [h('br'), h('small', state.params$().designation_desc)]) - : '' - ]) -} - -const wizardWrapper = state => { - return h('div.wizard-steps.donation-steps', [ - wizard.view(R.merge(state.wizard, { - steps: [ - {name: 'Amount', body: amountStep.view(state.amountStep)} - , {name: 'Confirm Card', body: paymentStep.view(state.paymentStep)} - - ] - , followup: followupStep.view(state) - })) - ]) -} - -module.exports = {view, init} \ No newline at end of file diff --git a/client/js/recurring_donations/edit/custom-nonprofit-branding.es6 b/client/js/recurring_donations/edit/custom-nonprofit-branding.es6 deleted file mode 100644 index 49b083d6..00000000 --- a/client/js/recurring_donations/edit/custom-nonprofit-branding.es6 +++ /dev/null @@ -1,4 +0,0 @@ -// License: LGPL-3.0-or-later -import nonprofitBranding from '../../../../javascripts/src/lib/nonprofitBranding.ts'; - -module.exports = nonprofitBranding diff --git a/client/js/recurring_donations/edit/followup-step.js b/client/js/recurring_donations/edit/followup-step.js deleted file mode 100644 index 26f82491..00000000 --- a/client/js/recurring_donations/edit/followup-step.js +++ /dev/null @@ -1,21 +0,0 @@ -// License: LGPL-3.0-or-later - -const h = require('snabbdom/h') - -function view(state) { - const supp = state.params$().supporter - return h('div.u-padding--10.u-centered', [ - h('h6.u-marginTop--15', 'Your donation was successful!') - , supp ? h('p', `A receipt will be emailed to ${supp.email}`) : '' - , h('hr') - , h('p', state.thankyou_msg || `${state.params$().nonprofit.name} appreciates your support!`) - // Show the 'finish' button only if we're in an offsite embedded modal - , h('div', [ - h('button.button', {on: {click: state.clickFinish$}}, 'Finish') - ]) - - ]) -} - - -module.exports = {view} diff --git a/client/js/recurring_donations/edit/get-params.js b/client/js/recurring_donations/edit/get-params.js deleted file mode 100644 index 813759f6..00000000 --- a/client/js/recurring_donations/edit/get-params.js +++ /dev/null @@ -1,23 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') - -const splitParam = str => - R.split(/[_;,]/, str) - -module.exports = params => { - const defaultAmts = '10,25,50,100,250,500,1000' - // Set defaults - const merge = R.merge({ - custom_amounts: '' - }) - // Preprocess data - const evolve = R.evolve({ - multiple_designations: splitParam - , custom_amounts: amts => R.compose(R.map(Number), splitParam)((amts instanceof String ? amts : R.map(x => x/100, amts).join(',')) || defaultAmts) - , custom_fields: fields => R.map(f => { - const [name, label] = R.map(R.trim, R.split(':', f)) - return {name, label: label ? label : name} - }, R.split(',', fields)) - }) - return R.compose(evolve, merge)(params) -} diff --git a/client/js/recurring_donations/edit/index.es6 b/client/js/recurring_donations/edit/index.es6 deleted file mode 100644 index 06f54116..00000000 --- a/client/js/recurring_donations/edit/index.es6 +++ /dev/null @@ -1,384 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const flyd = require('flyd') -const mergeAll = require('flyd/module/mergeall') -const flatMap = require('flyd/module/flatmap') -const lift = require('flyd/module/lift') -const snabbdom = require('snabbdom') -const h = require('snabbdom/h') -const R = require('ramda') -const render = require('ff-core/render') -const modal = require('ff-core/modal') -const notification = require('ff-core/notification') -const button = require('ff-core/button') -const request = require('../../common/request') -// local -const cardForm = require('../../components/card-form.es6') -const readableInterval = require('../../nonprofits/recurring_donations/readable_interval') -const format = require('../../common/format') -const supporterAddressForm = require('../../components/supporter-address-form.es6') -const changeAmountWizard = require('./change-amount-wizard.es6') - - -function init() { - var state = { - submitPaydate$: flyd.stream() - , confirmCancel$: flyd.stream() - , changeAmount$: flyd.stream() - , error$: flyd.stream() - } - - const rdPath = `/recurring_donations/${app.pageLoadData.recurring_donation.id}` - const rdUpdateAmountPath = `/recurring_donations/${app.pageLoadData.recurring_donation.id}/update_amount` - const token = utils.get_param('t') - state.donate_again_url = app.pageLoadData.miscellaneous_np_info.donate_again_url; - - // Paydate update and cancellation streams - const updatePaydate$ = flatMap(updatePaydate(rdPath), state.submitPaydate$) - const cancellation$ = flatMap(reqCancel(rdPath), state.confirmCancel$) - -state.changeAmountWizard = changeAmountWizard.init( {nonprofit:app.pageLoadData.nonprofit, - recurring_donation: app.pageLoadData.recurring_donation, - supporter: app.pageLoadData.supporter, - custom_amounts: app.pageLoadData.change_amount_suggestions}); - - state.cardForm = cardForm.init({ - card: { - name: app.pageLoadData.supporter.name - , address_zip: app.pageLoadData.supporter.zip_code - } - , path: '/cards' - , payload: { - edit_token: token - , path: rdPath - , card: { holder_id: app.pageLoadData.supporter.id, holder_type: 'Supporter'} - } - }) - - state.addressForm = supporterAddressForm.init({ - supporter: app.pageLoadData.supporter - , path: rdPath - , payload: { edit_token: token } - }) - - // Card update streams - // update the card id on the recurring donation after the card has been saved on CC - // (card-form.es6 component will post the card but will not update the card id on the recurring donation) - state.updateCardID$ = flatMap( - resp => request({ - method: 'put' - , path: rdPath - , send: {edit_token: token, token: resp.token, card_name: resp.name} - }).load - , state.cardForm.saved$ - ) - - - // Stream of notification messages - const message$ = flyd.mergeAll([ - flyd.map(R.always('Paydate successfully updated'), updatePaydate$) - , flyd.map(R.always('Address successfully updated'), state.addressForm.response$) - , flyd.map(R.always('Card successfully updated'), state.updateCardID$) - ]) - state.notification = notification.init({message$}) - - // A bunch of streams that cause the modal to close: - state.modalID$ = flyd.map( - R.always(null) - , mergeAll([ - updatePaydate$ - , state.updateCardID$ - , cancellation$ - , state.addressForm.response$ - ]) - ) - - // Stream of vals that cause loading animation to show/hide - state.loading$ = mergeAll([ - flyd.map(R.always(true), state.submitPaydate$) - , flyd.map(R.always(true), state.confirmCancel$) - , flyd.map(R.always(false), updatePaydate$) - , flyd.map(R.always(false), cancellation$) - ]) - - // Simply replace old recurring donations with new ones based on ajax responses - const setNew = (old, resp) => resp.body.recurring_donation - state.recDon$ = flyd.scanMerge([ - [updatePaydate$, setNew] - , [cancellation$, setNew] - , [state.updateCardID$, setNew] - , [state.updateCardAndAmount$, setNew] - ], app.pageLoadData.recurring_donation) - - return state -} - - -// -- Stream creator functions - -const updatePaydate = path => ev => { - ev.preventDefault() - const paydate = Number(ev.currentTarget.querySelector('input').value) - return request({ - method: 'PUT' - , path: path - , send: {edit_token: utils.get_param('t'), paydate: paydate} - }).load -} - - -const reqCancel = path => ev => { - ev.preventDefault() - return request({ - method: 'delete' - , path: path - , send: {edit_token: utils.get_param('t')} - }).load -} - - -// -- Virtual DOM functions - -function view(state) { - var rd = state.recDon$() - var supporter = state.addressForm.supporter$() - var status = rd.active ? 'Active' : 'Deactivated' - var interval = rd.active ? readableInterval(rd.interval, rd.time_unit) : 'Deactivated' - - return h('div.u-maxWidth--600.u-margin--auto.u-marginTop--50.u-padding--15.js-view-confirm', [ - h('h3.u-centered.u-marginBottom--20', ['Recurring Donation for ', String(supporter.name|| supporter.email)]) - // Show deactivated notification box if deactivated - , rd.active ? '' : h('p.u-centered.pastelBox--orange.u-padding--10.u-marginBottom--20', 'This recurring donation has been deactivated') - // Recurring Donation info table - , h('table.table--striped.u-marginBottom--50', [ - h('tr', [ - h('td.u-strong', 'Created on') - , h('td', format.date.toSimple(rd.created_at)) - ]) - , h('tr', [ - h('td.u-strong', 'Recurring amount') - , h('td', '$' + format.centsToDollars(rd.amount)) - ]) - , h('tr', [ - h('td.u-strong', 'Organization') - , h('td', [h('a', {props: {href: `/nonprofits/${rd.nonprofit_id}`, target: '_blank'}}, String(rd.nonprofit_name))]) - ]) - , h('tr', [ - h('td.u-strong', 'Card') - , h('td', String(rd.card_name)) - ]) - , h('tr', [ - h('td.u-strong', 'Donor email') - , h('td', String(app.pageLoadData.supporter.email)) - ]) - , h('tr', [ - h('td.u-strong', 'Recurring donation status') - , h('td', String(status)) - ]) - , h('tr', [ - h('td.u-strong', 'Recurring interval') - , h('td', String(interval)) - ]) - , rd.active - ? '' - : h('tr', [ - h('td.u-strong', 'Cancelled By') - , h('td', String(rd.cancelled_by)) - ]) - , rd.active - ? '' - : h('tr', [ - h('td.u-strong', 'Cancelled At') - , h('td', format.date.readableWithTime(rd.cancelled_at)) - ]) - , h('tr', [ - h('td.strong', 'Address') - , h('td', [ - h('small', [ - [supporter.address, supporter.city].filter(R.identity).join(', ') - , h('br') - , [supporter.state_code, supporter.zip_code, supporter.country].filter(R.identity).join(', ') - ]) - ]) - ]) - , rd.interval === 1 && rd.time_unit === 'month' - ? h('tr', [ - h('td.u-strong', 'Fixed paydate') - , h('td', String(rd.paydate ? rd.paydate : 'None')) - ]) - : '' - ]) - , actions(state) - , rd.active ? '' : reactivate(rd.nonprofit_id) - , cancelModal(state) - , updateCardModal(state) - , editPaydateModal(state) - , updateAddressModal(state) - , changeAmountModal(state) - , notification.view(state.notification) - ]) -} - - -const reactivate = np_id => - h('p.u-centered', [ h('a.button', {props: {href: `/nonprofits/${np_id}/donate`}}, 'Reactivate') ]) - - -function actions(state) { - var rd = state.recDon$() - if(!rd.active) return '' - var modalID$ = state.modalID$ - return h('div.pastelBox--looseleaf.u-padding--15.u-marginBottom--50', [ - h('p.u-strong.u-centered', 'What would you like to do?') - , h('ul.hasBullets.u-maxWidth--400.u-margin--auto', [ - h('li', [changeAmountBtn(modalID$)]) - , h('li', [updateCardBtn(modalID$)]) - - , h('li', [updateAddressBtn(modalID$)]) - , rd.interval === 1 && rd.time_unit === 'month' - ? h('li', [updatePaydateBtn(modalID$)]) - : '' - , h('li', [giveOneTimeDonationBtn(state)]) - , h('li', [cancelBtn(modalID$)]) - ]) - ]) -} - - -const changeAmountBtn = modalID$ => - h('strong', [ - h('a.test-changeAmount', { - on: {click: [modalID$, 'changeAmountModal']} - }, 'Change my donation amount') - ]) - -const updateCardBtn = modalID$ => - h('strong', [ - h('a.test-updateCard', { - on: {click: [modalID$, 'updateCardModal']} - }, 'Update my card') - ]) - - -const cancelBtn = modalID$ => - h('strong', [ - h('a.test-cancelDonation', { - on: {click: [modalID$, 'cancelRecDonModal']} - }, 'Cancel my recurring donation') - ]) - - -const updatePaydateBtn = modalID$ => - h('strong', [ - h('a', { - on: {click: [modalID$, 'editPaydateModal']} - }, 'Change the day I\'m billed') - ]) - - -const updateAddressBtn = modalID$ => - h('strong', [ - h('a', { - on: {click: [modalID$, 'updateAddressModal']} - }, 'Update my address') - ]) - - -const giveOneTimeDonationBtn = (state) => - h('strong', [ - h('a', { - props:{href: state.donate_again_url} - }, 'Give a one-time donation') - ]) - - -const cancelModal = state => - modal({ - thisID: 'cancelRecDonModal' - , id$: state.modalID$ - , body: h('div.u-marginTop--30.u-centered', [ - h('p.u-marginBottom--20', 'Cancelling your recurring donation will prevent any future charges for this donation.') - , h('hr.diamonds.u-marginBottom--40') - , h('p.u-strong', state.recDon$().nonprofit_name + ' will miss your support!') - , h('hr.diamonds') - , h('div.u-marginTop--30', [confirmCancelBtn(state)]) - ]) - }) - - -const updateCardModal = state => - modal({ - thisID: 'updateCardModal' - , id$: state.modalID$ - , title: 'Update Card' - , body: cardForm.view(state.cardForm) - }) - -const changeAmountModal = state => - modal({ - thisID: 'changeAmountModal' - , id$: state.modalID$ - , title: 'Change Amount' - , body: changeAmountWizard.view(state.changeAmountWizard) - }) - - -const editPaydateModal = state => - modal({ - thisID: 'editPaydateModal' - , id$: state.modalID$ - , title: 'Edit Paydate' - , body: paydateForm(state) - }) - - -const updateAddressModal = state => - modal({ - thisID: 'updateAddressModal' - , id$: state.modalID$ - , title: 'Edit your address' - , body: supporterAddressForm.view(state.addressForm) - }) - - -const paydateForm = state => - h('form', { on: {submit: state.submitPaydate$} }, [ - h('p', 'Enter a day of the month (between 1 and 28) when you want to be charged for this donation.') - , h('p', 'This will fix your donations to that date each month for all future payments.') - , h('input.input--small', { - props: { - type: 'number' - , max: 28 - , min: 1 - , name: 'paydate' - , value: state.recDon$().paydate || 1 - } - }) - , h('br') - , button(R.pick(['loading$', 'error$'], state)) - ]) - - -const confirmCancelBtn = state => - h('form', { on: { submit: state.confirmCancel$ } }, [ - button({ - buttonText: 'Cancel My Donation' - , loading$: state.loading$ - , error$: state.error$ - , buttonClass: 'red' - }) - ]) - - -// -- Render to the page - -var container = document.querySelector('#js-main') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -var state = init() -render({patch, view, state, container}) - diff --git a/client/js/recurring_donations/edit/page.js b/client/js/recurring_donations/edit/page.js deleted file mode 100644 index 0b4291b4..00000000 --- a/client/js/recurring_donations/edit/page.js +++ /dev/null @@ -1,2 +0,0 @@ -// License: LGPL-3.0-or-later -require("./index.es6") diff --git a/client/js/recurring_donations/edit/payment-step.es6 b/client/js/recurring_donations/edit/payment-step.es6 deleted file mode 100644 index 271c295e..00000000 --- a/client/js/recurring_donations/edit/payment-step.es6 +++ /dev/null @@ -1,105 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -flyd.lift = require('flyd/module/lift') -flyd.flatMap = require('flyd/module/flatmap') -const request = require('../../common/request') -const cardForm = require('./card-form.es6') -const format = require('../../common/format') -const progressBar = require('../../components/progress-bar') - -function init(params$, donation$) { - var state = { params$: params$, donation$: donation$ } - state.rdUpdateAmountPath = `/recurring_donations/${app.pageLoadData.recurring_donation.id}/update_amount` - state.token = utils.get_param('t') - - state.posting = false - - const cardPayload$ = flyd.map(supp => ({card: {holder_id: supp.id, holder_type: 'Supporter'}}), flyd.stream(state.params$().supporter)) - const card$ = flyd.merge( - flyd.stream({}) - , flyd.map(supp => ({name: supp.name, address_zip: supp.zip_code}), flyd.stream(state.params$().supporter))) - - state.cardForm = cardForm.init({path: '/cards', card$, payload$: cardPayload$, outerError$: state.error$}) - state.supporter$ = state.params$().supporter - // // Set the card ID into the donation object when it is saved - const cardToken$ = flyd.map(R.prop('token'), state.cardForm.saved$) - - state.updateCardAndAmount$ = flyd.flatMap( - resp => { - if(state.posting) return flyd.stream() - else state.posting = true - return request({ - method: 'put' - , path: state.rdUpdateAmountPath - , send: {edit_token: state.token, token: cardToken$(), amount: donation$().amount} - }).load} - , cardToken$ - ) - - - state.error$ = flyd.mergeAll([ - , flyd.map(R.always(undefined), state.cardForm.form.submit$) - , state.cardForm.error$ - , flyd.map(resp => "An unknown error occurred. Please try again later", flyd.filter(resp => - { - return resp.body.error || resp.status >= 300 - }, state.updateCardAndAmount$)) - ]) - - - - state.success$ = flyd.filter(resp => { - return !resp.body.error|| resp.status < 300 - }, state.updateCardAndAmount$) - - // Control progress bar - state.progress$ = flyd.scanMerge([ - [state.cardForm.form.validSubmit$, R.always({status: 'Checking card...', percentage: 20, hidden:false})] - , [state.cardForm.saved$, R.always({status: 'Finalizing...', percentage: 100, hidden:false})] - , [state.cardForm.error$, R.always({hidden: true, percentage: 0})] // Hide when an error shows up - , [flyd.filter(R.identity,state.error$), R.always({hidden: true})] // Hide when an error shows up - ], {hidden: true}) - - state.loading$ = flyd.mergeAll([ - flyd.map(R.always(true), state.cardForm.form.validSubmit$) - , flyd.map(R.always(false), state.cardForm.error$) - , flyd.map(R.always(false), state.error$) - , flyd.map(R.always(false), state.success$) - ]) - - - flyd.lift(() => state.posting = false, state.error$) - - flyd.lift((ev) => { - window.location.reload() - }, - state.success$) - - flyd.lift(() => { - console.log(state.error$()) - }, state.error$) - - return state -} - -function view(state) { - var isRecurring = true - var dedic = {} - return h('div.wizard-step.payment-step', [ - h('p.u-fontSize--18 u.marginBottom--0.u-centered', [ - h('span', '$' + format.centsToDollars(state.donation$().amount)) - , h('strong', isRecurring ? ' monthly recurring' : ' one-time ') - ]) - , dedic && (dedic.first_name || dedic.last_name) - ? h('p.u-centered', `In ${dedic.dedication_type || 'honor'} of ${dedic.first_name} ${dedic.last_name}`) - : '' - , h('div.u-marginBottom--10', [ - cardForm.view(R.merge(state.cardForm, {error$: state.error$, hideButton: state.loading$()})) - , progressBar(state.progress$()) - ]) - ]) -} - -module.exports = {view, init} diff --git a/client/js/recurring_donations/index.js b/client/js/recurring_donations/index.js deleted file mode 100644 index dbfae3ae..00000000 --- a/client/js/recurring_donations/index.js +++ /dev/null @@ -1,15 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../common/client') - -appl.def('update_card', function(form_obj) { - appl.is_loading() - - request.put('/recurring_donations/' + form_obj.recurring_donation_id) - .send({stripe_card: form_obj}) - .end(function(err, resp){ - appl.def('loading', false) - if(!resp.ok) return appl.notify('Unable to update card. Please contact us at support@commitchange.com.') - appl.notify('Card Updated!') - }) -}) - diff --git a/client/js/refunds/create.js b/client/js/refunds/create.js deleted file mode 100644 index d19153b8..00000000 --- a/client/js/refunds/create.js +++ /dev/null @@ -1,69 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const format = require('../common/format') -var format_err = require('../common/format_response_error') -var request = require('../common/super-agent-promise') - -appl.def('ajax_refunds', { - create: function(charge_id, form_obj, node) { - form_obj = formatter(form_obj) - appl.def({ - loading: true, - refunds: { error: '', loading: true } - }) - post_refund(charge_id, form_obj) - .then(function(resp) { - not_loading() - appl.close_modal() - return resp - }) - .then(function(resp) { return resp.body }) - .then(fetch_data_on_success) - .then(display_success_message) - .catch(show_err) - } -}) - -const formatter = R.evolve({ - amount: format.dollarsToCents -}) - -// Re-fetch all the payment data on the page after a refund has been made -function fetch_data_on_success(refund) { - appl.payments.index() - appl.ajax_payment_details.fetch(appl.payment_details.data.id) - return refund -} - -// Display a nice message confirming the amounts of the refund they just made -function display_success_message(refund) { - appl.notify( - "Your refund was successful!" - ) - return refund -} - -// Reset the loading state in the ui -function not_loading(x) { - appl.def({loading: false, refunds: {loading: false}}) - return x -} - -// Display an error in the ui -function show_err(resp) { - not_loading() - console.warn('Error in promise chain: ', resp) - appl.def('refunds', { - error: format_err(resp), - loading: false - }) -} - -// Make the ajax request, returning a Promise -function post_refund(charge_id, obj) { - return request - .post('/nonprofits/' + app.nonprofit_id + '/charges/' + charge_id + '/refunds') - .send({refund: obj}) - .perform() -} - diff --git a/client/js/settings/index/branding/index.js b/client/js/settings/index/branding/index.js deleted file mode 100644 index 10ee8272..00000000 --- a/client/js/settings/index/branding/index.js +++ /dev/null @@ -1,53 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const snabbdom = require('snabbdom') -const flyd = require('flyd') -const R = require('ramda') -const render = require('ff-core/render') -flyd.flatMap = require('flyd/module/flatmap') -flyd.filter = require('flyd/module/filter') -flyd.mergeAll = require('flyd/module/mergeall') -const notification = require('ff-core/notification') -// local -const fonts = require('../../../common/brand-fonts') -const request = require('../../../common/request') -const colorPicker = require('../../../components/color-picker.es6') -const view = require('./view') - -function init() { - var np = R.merge(app.nonprofit, {}) - var state = { - nonprofit: np - , font$: flyd.stream({ - key: np.brand_font || 'bitter' - , family: np.brand_font ? fonts[np.brand_font].family : fonts.bitter.family - , name: np.brand_font ? fonts[np.brand_font]['name'] : 'Bitter' - }) - , color: np.brand_color - , submit$: flyd.stream() - , color$: flyd.stream() - } - - const resp$ = flyd.flatMap( - state => flyd.map(R.prop('body'), request({ - method: 'put' - , path: `/nonprofits/${np.id}` - , send: {nonprofit: { brand_color: state.colorPicker.color$(), brand_font: state.font$().key }} - }).load) - , state.submit$) - - var notify$ = flyd.map(()=> 'We successfully saved your branding settings!', resp$) - - state.loading$ = flyd.mergeAll([ - flyd.map(()=> true, state.submit$) - , flyd.map(()=> false, resp$) - ]) - - state.notification = notification.init({message$: notify$}) - state.colorPicker = colorPicker.init(state.nonprofit.brand_color) - - return state -} - -module.exports = {view, init} - diff --git a/client/js/settings/index/branding/view.js b/client/js/settings/index/branding/view.js deleted file mode 100644 index ff177fe2..00000000 --- a/client/js/settings/index/branding/view.js +++ /dev/null @@ -1,79 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const h = require('snabbdom/h') -const R = require('ramda') -const notification = require('ff-core/notification') -const button = require('ff-core/button') -// local -const colorPicker = require('../../../components/color-picker.es6') -const fonts = require('../../../common/brand-fonts') - - -const message = 'This branding will be applied to your donate buttons, profile page, campaign pages and event pages' - -var updatedTask = "Select a color and font to make your fundraising tools consistent with your brand." -const view = state => - h('section.branding.settings-pane.nonprofit-settings.hide', [ - h('header.pane-header', [h('h3', 'Branding')]) - , h('div.pane-inner.branding', [ - h('p.pastelBox--yellow.u-padding--10', message) - , h('br') - , h('div.branding-settings-wrapper', [ - colorPickWrap(state) - , fontPicker(state) - , form(state) - ]) - , preview(state) - ]) - , notification.view(state.notification) - ]) - -const colorPickWrap = state => - h('div.color-wrapper', [ - h('p.title', 'Select Brand Color') - , colorPicker.view(state.colorPicker) - , h('div.colPick-wrapper.inner#colorpicker') - ]) - -const fontPicker = state => - h('div.font-wrapper', [ - h('p.title', 'Select Brand Font') - , fontListing(state) - ]) - -const fontListing = state => - h('ul.inner', R.map(R.apply(fontRow(state)), R.toPairs(fonts))) - -const fontRow = R.curry((state, key, font) => - h('li', { - style: { fontFamily: font.family } - , on: {click: [state.font$, R.merge(font, {key: key})]} - }, font.name) -) - -const form = state => { - var btn = button({ buttonText: 'Save Branding' , loading$: state.loading$ }) - - return h('form.branding-form', { - on: {submit: ev => {ev.preventDefault(); state.submit$(state)}} - }, [btn]) -} - -const preview = state => - h('div.preview-wrapper', [ - h('p.title', 'Preview') - , previewDonateBtn(state) - ]) - -const previewDonateBtn = state => - h('div.branded-donate-button-wrapper', [ - h('p.branded-donate-button', { - style: { - background: state.colorPicker.color$() - , fontFamily: state.font$().family - } - }, 'Donate' ) - ]) - -module.exports = view - diff --git a/client/js/settings/index/email-settings/index.js b/client/js/settings/index/email-settings/index.js deleted file mode 100644 index 6dcb4ca2..00000000 --- a/client/js/settings/index/email-settings/index.js +++ /dev/null @@ -1,45 +0,0 @@ -// License: LGPL-3.0-or-later -// npm -const snabbdom = require('snabbdom') -const flyd = require('flyd') -const R = require('ramda') -const render = require('ff-core/render') -const notification = require('ff-core/notification') -const serializeForm = require('form-serialize') -flyd.flatMap = require('flyd/module/flatmap') -flyd.mergeAll = require('flyd/module/mergeall') - -// local -const request = require('../../../common/request') -const view = require('./view') - -function init() { - var state = { submit$: flyd.stream() } - - // formSerialize will set checked boxes to "on" and unchecked boxes to "". We want it to be true/false instead - const formObj$ = R.compose( - flyd.map(obj => R.map(val => val === 'on' ? true : false, obj)) - , flyd.map(ev => serializeForm(ev.currentTarget, {hash: true, empty: true})) - )(state.submit$) - - const path = `/nonprofits/${app.nonprofit_id}/users/${app.current_user_id}/email_settings` - - const updateResp$ = flyd.flatMap( - obj => request({ path, method: 'post' , send: {email_settings: obj} }).load - , formObj$ ) - - state.email_settings$ = flyd.map(R.prop('body'), request({method: 'get', path}).load) - - state.loading$ = flyd.mergeAll([ - flyd.map(R.always(true), state.submit$) - , flyd.map(R.always(false), updateResp$) - ]) - - const notify$ = flyd.map(()=> 'Email notification settings updated.', updateResp$) - state.notification = notification.init({message$: notify$}) - - return state -} - -module.exports = {init, view} - diff --git a/client/js/settings/index/email-settings/view.js b/client/js/settings/index/email-settings/view.js deleted file mode 100644 index 493dad37..00000000 --- a/client/js/settings/index/email-settings/view.js +++ /dev/null @@ -1,61 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const button = require('ff-core/button') -const notification = require('ff-core/notification') - -const view = state => { - var settings = state.email_settings$() - if(!settings) { - return h('section.settings-pane.nonprofit-settings.notifications.hide', [h('i.fa.fa-spin.fa-spinner'), ' Loading...']) - } - return h('section.settings-pane.nonprofit-settings.notifications.hide', [ - h('header.pane-header', [h('h3', 'Email Notifications')]) - , h('p', `Choose the emails you want to receive for ${app.user.email}`) - , h('form.notificationsForm', {on: {submit: ev=> {ev.preventDefault(); state.submit$(ev)}}}, [ - h('fieldset', [ - h('input#notifications_payments', {props: {type: 'checkbox', name: 'notify_payments', checked: settings.notify_payments}}) - , h('label', {props: {htmlFor: 'notifications_payments'}}, [ - h('strong', 'General payment notifications') - , h('br') - , h('small', 'Receive donation receipts, ticket receipts, and refund notifications') - ]) - ]) - , h('fieldset', [ - h('input#notifications_campaigns', {props: {type: 'checkbox', name: 'notify_campaigns', checked: settings.notify_campaigns}}) - , h('label', {props: {htmlFor: 'notifications_campaigns'}}, [ - h('strong', 'Campaign notifications') - , h('br') - , h('small', 'Receive all campaign receipts by default (you can also enable/disable these emails within the settings for each campaign page)') - ]) - ]) - , h('fieldset', [ - h('input#notifications_events', {props: {type: 'checkbox', name: 'notify_events', checked: settings.notify_events}}) - , h('label', {props: {htmlFor: 'notifications_events'}}, [ - h('strong', 'Event notifications') - , h('br') - , h('small', 'Receive all event receipts by default (you can also enable/disable these emails within the settings for eachp event page)') - ]) - ]) - , h('fieldset', [ - h('input#notifications_payouts', {props: {type: 'checkbox', name: 'notify_payouts', checked: settings.notify_payouts}}) - , h('label', {props: {htmlFor: 'notifications_payouts'}}, [ - h('strong', 'Payout notifications') - , h('br') - , h('small', 'Receive notifications about pending, succeeded, and/or failed payouts') - ]) - ]) - , h('fieldset', [ - h('input#notifications_recurring_donations', {props: {type: 'checkbox', name: 'notify_recurring_donations', checked: settings.notify_recurring_donations}}) - , h('label', {props: {htmlFor: 'notifications_recurring_donations'}}, [ - h('strong', 'Recurring donation cancellation notifications') - , h('br') - , h('small', 'Receive emails when a donor cancels their recurring donation') - ]) - ]) - , button({loading$: state.loading$}) - , notification.view(state.notification) - ]) - ]) -} - -module.exports = view diff --git a/client/js/settings/index/integrations/index.js b/client/js/settings/index/integrations/index.js deleted file mode 100644 index 8ae97776..00000000 --- a/client/js/settings/index/integrations/index.js +++ /dev/null @@ -1,60 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const request = require('../../../common/request') -const flyd_lift = require('flyd/module/lift') -const colors = require('../../../common/colors') - -function init() { - var state = { - clickSync$: flyd.stream() - , mailchimpKeyResp$: request({method: 'get', path: `/nonprofits/${app.nonprofit_id}/nonprofit_keys`, query: {select: 'mailchimp_token'}}).load - } - state.mailchimpKey$ = flyd.map(R.prop('response'), flyd.filter(resp => resp.status === 200, state.mailchimpKeyResp$)) - return state -} - - -function view(state) { - return h('section.integrations.settings-pane.nonprofit-settings', { - style: {display: 'none'} - }, [ - h('header.pane-header', [h('h3', 'Integrations')]) - , h('p', 'Connect your CommitChange account with other apps to take advantage of integration features.') - , integrationSection(state, state.mailchimpKey$(), 'MailChimp', mailchimpConnectedMessage, mailchimpNotConnectedMessage) - , h('br') - ]) -} - -// A section fo each integration; pass in the API key for the integration, the -// name, and two functions for bodies: the first body function for when the API -// key is defined, the second body function for when the API key is undefined -const integrationSection = (state, key, name, bodyConnected, bodyNotConnected) => { - return h('div.pane-inner.integrations', [ - h('h6', { - style: {color: key ? colors['$bluegrass'] : colors['$grey']} - }, [ - key ? h('i.fa.fa-check') : h('i.fa.fa-question-circle') - , ' ' + name - ]) - , key ? bodyConnected(state) : bodyNotConnected(state) - ]) -} - -const mailchimpNotConnectedMessage = state => { - return h('p', [ - 'Connect with MailChimp to automatically sync supporter emails to your MailChimp Email Lists.' - , h('br') - , h('a', {props: {href: `/nonprofits/${app.nonprofit_id}/nonprofit_keys/mailchimp_login`}}, 'Click here to connect your Mailchimp account.') - ]) -} -const mailchimpConnectedMessage = state => { - return h('p', [ - 'Congrats! You Mailchimp account has been connected successfully.' - , h('br') - , h('a', {props: {href: `/nonprofits/${app.nonprofit_id}/supporters?show-modal=mailchimpSettingsModal`}}, 'Click here to manage your email list sync settings.') - ]) -} - -module.exports = {view, init} diff --git a/client/js/settings/index/page.js b/client/js/settings/index/page.js deleted file mode 100644 index 16358647..00000000 --- a/client/js/settings/index/page.js +++ /dev/null @@ -1,134 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../../common/client') -require('../../common/image_uploader') -require('../../common/el_swapo') -require('../../common/restful_resource') - -const render = require('ff-core/render') -const h = require('snabbdom/h') -const R = require('ramda') -const flyd = require('flyd') -const snabbdom = require('snabbdom') -const branding = require('./branding/index') -const emailSettings = require('./email-settings/index') -const integrations = require('./integrations/index') - -function init() { - var state = {} - state.emailSettings = emailSettings.init() - state.branding = branding.init() - state.integrations = integrations.init() - return state -} - -function view(state) { - return h('div', [ - emailSettings.view(state.emailSettings) - , branding.view(state.branding) - , integrations.view(state.integrations) - ]) -} - -// -- Render flimflam - -var container = document.querySelector('#js-main') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -var state = init() -render({patch, view, state, container}) - - - -// Initialize the froala wysiwyg -appl.def('initialize_froala', function(){ - var editable = require('../../common/editable') - editable($('.editable'), { - email_buttons: true, - placeholder: 'Edit donation receipt message here.', - sticky: true, - noUpdateOnChange: true - }) -}) - -var np_route = '/nonprofits/' + app.nonprofit_id - -appl.def('update_card_failure_message', function() { - appl.def('card_failure_message.loading', true) - var messageTop = document.getElementById('js-messageTop').innerHTML - var messageBottom = document.getElementById('js-messageBottom').innerHTML - var data = { nonprofit: { - card_failure_message_top: messageTop - ,card_failure_message_bottom: messageBottom - } - } - request.put(np_route + '.json').send(data).end(function(resp) { - appl.notify('Card failure email successfully saved') - appl.def('card_failure_message.loading', false) - }) -}) - -appl.def('update_custom_receipt', function(node) { - appl.def('receipt.loading', true) - var classToFind = getClassToFindEditor() - var receipt = appl.prev_elem(node).getElementsByClassName(classToFind)[0].innerHTML - var data = { nonprofit: {thank_you_note: receipt} } - request.put(np_route + '.json').send(data).end(function(resp) { - appl.notify('Receipt successfully saved') - appl.def('receipt.loading', false) - }) -}) - -appl.def('update_change_amount_message', function(node) { - appl.def('receipt.loading', true) - var classToFind = getClassToFindEditor() - var msg = appl.prev_elem(node).getElementsByClassName(classToFind)[0].innerHTML - var data = { miscellaneous_np_info: {change_amount_message: msg} } - request.put(np_route + '/miscellaneous_np_info.json').send(data).end(function(resp) { - appl.notify('Change amount message saved') - appl.def('receipt.loading', false) - }) -}) - -if(app.current_nonprofit_user) { - appl.verify_identity = require('../../nonprofits/payouts/index/verify_identity') - appl.create_bank_account = require('../../bank_accounts/create.es6') -} - -appl.def('statement.validate', function(node) { - var statement_val = appl.prev_elem(node).value - appl.def('statement.name', statement_val) - if(statement_val.search(/[^\w+(<{@?&!$;:\.\-\'\"\,\s}>)]/gi) < 0) { - appl.def('statement.invalid', false) - appl.def('error', '') - } - else { - appl.def('statement.invalid', true) - appl.def('error', 'Statement name cannot contain special characters') - } -}) - -appl.def('cancel_billing_subscription', function() { - appl.notify('Cancelling subscription...') - appl.def('loading', true) - request.put(np_route + '/billing_subscription/cancel') - .send({}).end(function(resp) { - appl.def('loading', false) - }) -}) - -function getClassToFindEditor() -{ - if (app.editor === 'froala' ) - return "froala-element" - else if (app.editor === 'quill') - return "ql-editor" -} - -window.onload = function() { - appl.initialize_froala() -} - diff --git a/client/js/stripe_wrapper/index.es6 b/client/js/stripe_wrapper/index.es6 deleted file mode 100644 index dc9ac2dc..00000000 --- a/client/js/stripe_wrapper/index.es6 +++ /dev/null @@ -1,54 +0,0 @@ -// License: LGPL-3.0-or-later -const jQuery = require ('jquery') - -/** - * A wrapper for replicating Stripe.js v2's tokenizing features - * with a compatible API. It allows a service provider to use fully free software - * for Stripe integration. Whether that meets your needs is up to you :) - * - * To use it set the `payment_provider.stripe_proprietary_v2_js` to `false` - * (which is the default in settings) - */ -class Stripe { - - setPublishableKey(key) { - - this.card = new TokenizerWrapper( 'card', key) - this.bankAccount = new TokenizerWrapper('bank_account',key) - } -} - -class TokenizerWrapper { - constructor( inner_field_name, key) - { - this.inner_field_name = inner_field_name - this.key = key - } - - createToken(outer_obj, callback) { - var self = this - var auth = 'Bearer '+ self.key - - - var inner_field_name = self.inner_field_name - - var obj = {} - - obj[inner_field_name] = outer_obj - - jQuery.ajax('https://api.stripe.com/v1/tokens', { - headers: { - 'Authorization': auth, - 'Accept': 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded'}, - method: 'POST', - data: obj - }).done((data, textStatus, jqXHR) => { - callback(jqXHR.status, data) - }).fail((jqXHR, textStatus, errorThrown) => { - callback(jqXHR.status, jqXHR.responseJSON) - }) - } -} - -global.Stripe = new Stripe() \ No newline at end of file diff --git a/client/js/stripe_wrapper/page.js b/client/js/stripe_wrapper/page.js deleted file mode 100644 index 0b4291b4..00000000 --- a/client/js/stripe_wrapper/page.js +++ /dev/null @@ -1,2 +0,0 @@ -// License: LGPL-3.0-or-later -require("./index.es6") diff --git a/client/js/super-admin/fullcontact-table.js b/client/js/super-admin/fullcontact-table.js deleted file mode 100644 index c15f7800..00000000 --- a/client/js/super-admin/fullcontact-table.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -const h = require('flimflam/h') -const searchTable = require('../components/search-table') - -const row = (data) => { - const results = JSON.stringify(data, null, 2) - return h('pre', results) -} - -module.exports = state => searchTable(state, [], row, 'Search by email') - diff --git a/client/js/super-admin/nonprofits-table.js b/client/js/super-admin/nonprofits-table.js deleted file mode 100644 index 8efa9c47..00000000 --- a/client/js/super-admin/nonprofits-table.js +++ /dev/null @@ -1,67 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') -const searchTable = require('../components/search-table') - -const header = [ - h('tr', [ - h('th', '') - , h('th.pl-0', 'Info') - , h('th.sm-hide', 'Processed') - , h('th.sm-hide', 'Links') - ]) -] - -const link = (href, text) => h('p.m-0', [ h('a', {props: {href, target: '_blank'}}, text)]) - -const npoLinkCurry = id => (path, text) => link(`/nonprofits/${id}/${path}`, text ? text : path) - -const links = (npoLink, data) => - h('div', [ - npoLink('payments') - , npoLink('supporters') - , npoLink('settings') - , npoLink('campaigns', 'campaigns: ' + data.campaigns_count) - , npoLink('events', 'events: ' + data.events_count) - , link('https://dashboard.stripe.com/search?query=' + data.stripe_account_id, 'Stripe account') - , data.stripe_customer_id - ? link('https://dashboard.stripe.com/search?query=' + data.stripe_customer_id, 'Stripe customer') - : '' - ]) - -const processed = data => - h('div', [ - h('p.m-0.bold', data.total_processed || '$0.00') - , h('p.m-0.bold.color-green', data.total_fees || '$0.00') - , h('p.m-0', (100 * data.percentage_fee).toFixed(1) + '%') - ]) - -const row = (data={}, i) => { - const npoLink = npoLinkCurry(data.id) - return h('tr.sub', [ - h('td.content-width.color-grey', ++i + '.') - , h('td.pl-0', [ - h('h5.m-0.max-width-1', [npoLink('', - data.name + ' (' + data.state_code + ')')]) - , h('p.m-0', '#' + data.id) - , h('p.m-0', data.email || '') - , h('p.m-0', data.created_at) - , h('p.m-0.color-red', [ - h('span', {class: { 'color-green' : data.vetted }} - , data.vetted ? 'vetted' : 'not vetted') - , h('span.color-grey.mx-1', ' | ') - , h('span', {class: { 'color-green' : data.verification_status === 'verified' }} - , data.verification_status || '') - ]) - , h('div.md-hide.lg-hide', [ - processed(data) - , links(npoLink, data) - ]) - ]) - , h('td.sm-hide', [processed(data)]) - , h('td.sm-hide', [links(npoLink, data)]) - ]) -} - -module.exports = state => searchTable(state, header, row, 'Search NPOs') - diff --git a/client/js/super-admin/page.js b/client/js/super-admin/page.js deleted file mode 100644 index a3491c7c..00000000 --- a/client/js/super-admin/page.js +++ /dev/null @@ -1,48 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') -const flyd = require('flimflam/flyd') -const render = require('flimflam/render') -const tabswap = require('flimflam/ui/tabswap') - -const nposTable = require('./nonprofits-table') -const profilesTable = require('./profiles-table') -const fullContactTable = require('./fullcontact-table') -const topNav = require('../components/top-nav') -const searchData = require('../common/search-data') - -const init = () => { - const activeTab$ = flyd.stream(0) - const pageLength = 30 - const nposData = searchData('admin/search-nonprofits', pageLength) - const profilesData = searchData('admin/search-profiles', pageLength) - const fullContactData = searchData('admin/search-fullcontact', pageLength) - - return { - activeTab$ - , nposData - , profilesData - , fullContactData - } -} - -const view = state => - h('div', [ - topNav('Super Admin') - , h('div.container.pt-3', [ - tabswap.labels({ names: ['NPOs', 'Profiles', 'FC'], active$: state.activeTab$}) - ]) - , h('div.container.px-2.pb-3', [ - tabswap.content({ sections: [ - [nposTable(state.nposData)] - , [profilesTable(state.profilesData)] - , [fullContactTable(state.fullContactData)] - ] - , active$: state.activeTab$}) - ]) - ]) - -const container = document.getElementById('ff-render-super-admin') - -render(view, init(), container) - diff --git a/client/js/super-admin/profiles-table.js b/client/js/super-admin/profiles-table.js deleted file mode 100644 index 040d60c4..00000000 --- a/client/js/super-admin/profiles-table.js +++ /dev/null @@ -1,48 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('flimflam/h') -const searchTable = require('../components/search-table') -const request = require('../common/client') - -const link = (href, text) => h('p.m-0', [ h('a', {props: {href, target: '_blank'}}, text)]) - - - -const row = (data={}, i) => { - const sendUserConfirmation = (user_id) => - { - request.get(`/admin/resend_user_confirmation`).query({profile_id: data.id}).end((err, result) => - { - if (err) - { - window.alert(`Uh oh, we have a bug! Error is in browser console (Ctrl-Shift-i) and listed next: ${err}`) - console.error(err) - } - else { - - window.alert("Confirmation sent!") - } - - - }); - }; - - const name = data.name ? data.name : 'No name' - return h('tr.sub', [ - h('td.content-width.color-grey', ++i + '.') - , h('td.pl-0', [ - h('h5.m-0.max-width-1', [link(`/profiles/${data.id}/`, name)]) - , h('p.m-0', '#' + data.id) - , data.email ? h('p.m-0', data.email) : '' - , data.city ? h('p.m-0', data.city) : '' - , data.created_at - , h('p.m-0', {class: { - 'color-green' : data.is_confirmed - , 'color-red' : !data.is_confirmed }} - , data.is_confirmed ? 'confirmed' : [h('a', {on: {click: () => {sendUserConfirmation(data.id)}}}, 'unconfirmed')]) - ]) - ]) -} - -module.exports = state => searchTable(state, [], row, 'Search profiles') - diff --git a/client/js/supporters/index.js b/client/js/supporters/index.js deleted file mode 100644 index faaa173d..00000000 --- a/client/js/supporters/index.js +++ /dev/null @@ -1,67 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const render = require('ff-core/render') -const request = require('../common/request') -const snabbdom = require('snabbdom') -const flyd_lift = require('flyd/module/lift') -const flyd_mergeAll = require('flyd/module/mergeall') -const url = require('url') - -// TODO move this into sub-component in the future -const mailchimpModal = require('./settings/mailchimp-integration-settings') - -// This is the root component for the supporters dashboard/CRM, found on /nonprofits/:nonprofit_id/supporters - -function init() { - var state = { } - var thisUrl = url.parse(location.href, true) - const mailchimpSyncClick$ = getMailchimpClickSync() - const mailchimpKeyResp$ = request({method: 'get', path: `/nonprofits/${app.nonprofit_id}/nonprofit_keys`, query: {select: 'mailchimp_token'}}).load - const hasKey$ = flyd.filter(resp => resp.status === 200, mailchimpKeyResp$) - const modalID$ = flyd_mergeAll([ - flyd_lift(openModalOrAuth, mailchimpSyncClick$, mailchimpKeyResp$) - , flyd.map(()=> thisUrl.query['show-modal'], hasKey$) - ]) - state.mailchimpModal = mailchimpModal.init(modalID$) - return state -} - -// Either return the modal ID to open, or redirect the page to the mailchimp oauth screen -const openModalOrAuth = (ev, resp) => { - if(resp.status === 200) { - return 'mailchimpSettingsModal' - } else { - window.location.href = `/nonprofits/${app.nonprofit_id}/nonprofit_keys/mailchimp_login` - return null - } -} - -const getMailchimpClickSync = () => { - const s = flyd.stream() - document.querySelector('.js-openMailchimpModal') - .addEventListener('click', ev => {appl.close_modal(); s(ev)}) - return s -} - - -function view(state) { - return h('div', [ - mailchimpModal.view(state.mailchimpModal) - ]) -} - - -// -- Render to the page - -var container = document.querySelector('#js-main') -const patch = snabbdom.init([ - require('snabbdom/modules/eventlisteners') -, require('snabbdom/modules/class') -, require('snabbdom/modules/props') -, require('snabbdom/modules/style') -]) -var state = init() -render({patch, view, state, container}) - diff --git a/client/js/supporters/info-card.es6 b/client/js/supporters/info-card.es6 deleted file mode 100644 index b263e762..00000000 --- a/client/js/supporters/info-card.es6 +++ /dev/null @@ -1,111 +0,0 @@ -// License: LGPL-3.0-or-later -const request = require("../common/super-agent-frp") -const view = require("vvvview") -const flyd = require('flyd') -const flatMap = require('flyd/module/flatmap') -const scanMerge = require('flyd/module/scanmerge') -const h = require('virtual-dom/h') -const Im = require('immutable') -const Map = Im.Map -const fromJS = Im.fromJS -const OrderedMap = Im.OrderedMap -const format = require('../common/format') - -var state = fromJS({is_visible: false, data: {}, coords: {top: 0, right: 0, left: 0}}) - -var $showClicks = flyd.stream() -var $hideClicks = flyd.stream() - -const root = state => { - if(state.get('is_visible') && state.get('data')) { - return h('aside.infoCard', { - style: { - display: state.get('is_visible') ? 'block' : 'none', - top: state.getIn(['coords', 'top']) - state.get('rows') * 23 + 'px', - left: state.getIn(['coords', 'left']) + 'px', - right: state.getIn(['coords', 'right']) + 'px' - } - }, [ - h('i.fa.fa-times', {onclick: $hideClicks}), - supporterTable(state.get('data')), - h('a.button--micro', {href: state.getIn(['data', 'link'])}, 'View Full Details') - ]) - } else return h('span') -} - -const supporterDetail = pair => { - var [key, val] = pair - if(key === 'link') return '' - return val ? h('tr', [h('td', format.snake_to_words(key)), h('td', val)]) : '' -} - -const supporterTable = supporter => - h('table', supporter.entrySeq().map(supporterDetail).toJS()) - -const displayCard = (state, node) => { - var clientTop = document.documentElement.clientTop - var clientLeft = document.documentElement.clientLeft - var box = node.getBoundingClientRect() - var top = box.top + window.pageYOffset - clientTop - var left = box.left + window.pageXOffset - clientLeft - - // Place card 15px from right when it's too far over. - if(left + 350 >= document.body.offsetWidth) { - state = state.setIn(['coords', 'right'], 15) - state = state.setIn(['coords', 'left'], 'initial') - } else { - state = state.setIn(['coords', 'left'], left) - state = state.setIn(['coords', 'right'], 'initial') - } - - return state - .setIn(['coords', 'top'], top) - .set('is_visible', true) - .set('data', false) -} - -// Count the number of rows of data the supporter has -const calculateRows = state => - state.set('rows', state.get('data').entrySeq().filter(pair => { - var [key, val] = pair - return val && String(val).length - }).count() + 1.5) - -const ajaxSupporter = node => { - var id = node.getAttribute('data-id') - return request.get(`/nonprofits/${app.nonprofit_id}/supporters/${id}/info_card`).perform() -} - -var $responses = flatMap(ajaxSupporter, $showClicks) - -const setSupporterData = (state, response) => { - var d = response.body - if(!d) return state - state = state.set('data', OrderedMap({ - name: d.name - , email: d.email - , phone: utils.pretty_phone(d.phone) - , address: utils.address_with_commas(d.address, d.city, d.state_code, d.zip_code,d.country) - , organization: d.organization - , total_raised: '$' + utils.cents_to_dollars(d.raised) - , link: `/nonprofits/${app.nonprofit_id}/supporters?sid=${d.id}/` - })) - // Count the rows of present data to calculate the card height - state = calculateRows(state) - return state -} - -var $state = flyd.immediate(scanMerge([ - [$hideClicks, state => state.set('is_visible', false)], - [$showClicks, displayCard], - [$responses, setSupporterData], -], state)) - -var infoCard = view(root, document.body, state) - -flyd.map(infoCard, $state) - - -// XXX viewscript lol -appl.def('show_supporter_info_card', function(node){ $showClicks(appl.prev_elem(node)) }) - diff --git a/client/js/supporters/settings/mailchimp-integration-settings.js b/client/js/supporters/settings/mailchimp-integration-settings.js deleted file mode 100644 index 7fe3781a..00000000 --- a/client/js/supporters/settings/mailchimp-integration-settings.js +++ /dev/null @@ -1,86 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const h = require('snabbdom/h') -const flyd = require('flyd') -const modal = require('ff-core/modal') -const button = require('ff-core/button') -const request = require('../../common/request') -const serialize = require('form-serialize') -const notification = require('ff-core/notification') -const flyd_flatMap = require('flyd/module/flatmap') -const flyd_mergeAll = require('flyd/module/mergeall') - -function init(modalID$) { - const pathPrefix = `/nonprofits/${app.nonprofit_id}` - var state = { - submitForm$: flyd.stream() - , tagMasters$: flyd.map(R.prop('body'), request({method: 'get', path: pathPrefix + '/tag_masters'}).load) - } - - const emailLists$ = flyd.map(R.prop('body'), request({method: 'get', path: pathPrefix + '/email_lists'}).load) - state.selectedTagMasterIds$ = flyd.map(R.map(ls => ls.tag_master_id), emailLists$) - - const response$ = flyd_flatMap( - form => request({ - method: 'post' - , path: `/nonprofits/${app.nonprofit_id}/email_lists` - , send: {tag_masters: serialize(form, {hash: true})} - }).load - , state.submitForm$ ) - - state.loading$ = flyd_mergeAll([ - flyd.map(()=> false, response$) - , flyd.map(()=> true, state.submitForm$) - ]) - - state.modalID$ = flyd_mergeAll([ - modalID$ - , flyd.map(()=> null, response$) - ]) - - const message$ = flyd_mergeAll([ - flyd.map(()=> 'Tags successfully synced! Your email lists should show on MailChimp within 5-10 minutes', response$) - ]) - state.notification = notification.init({message$}) - - return state -} - - -function view(state) { - var body = h('form', {on: {submit: ev => {ev.preventDefault(); state.submitForm$(ev.currentTarget)}}}, [ - h('p', "You're connected on Mailchimp. Choose the tags that you want to keep in sync with your Mailchimp Email Lists.") - , h('hr') - , h('div.fields', - R.map( - tm => h('fieldset', [ - h('input', { - props: { - type: 'checkbox' - , name: tm.name - , value: tm.id - , id: `mailchimpCheckbox--${tm.id}` - , checked: (state.selectedTagMasterIds$()||[]).indexOf(tm.id) !== -1 - } - }) - , h('label', {props: {htmlFor: `mailchimpCheckbox--${tm.id}`}}, tm.name) - ]) - , (state.tagMasters$() || {data: []}).data ) - ) - , h('hr') - , h('div.u-centered', [ - button({loading$: state.loading$}) - ]) - ]) - return h('div', [ - modal({ - thisID: 'mailchimpSettingsModal' - , id$: state.modalID$ - , title: 'MailChimp Sync' - , body - }) - , notification.view(state.notification) - ]) -} - -module.exports = {view, init} diff --git a/client/js/ticket_levels/get_totals.js b/client/js/ticket_levels/get_totals.js deleted file mode 100644 index 0685062e..00000000 --- a/client/js/ticket_levels/get_totals.js +++ /dev/null @@ -1,11 +0,0 @@ -// License: LGPL-3.0-or-later -// Retrieve the total attendee (ticket) counts for every ticket level for a given event -var request = require("../common/super-agent-promise") - -module.exports = get_totals - -function get_totals(nonprofit_id, event_id) { - return request.get('/nonprofits/' + nonprofit_id + '/events/' + event_id + '/ticket_levels') - .perform() -} - diff --git a/client/js/ticket_levels/manage.js b/client/js/ticket_levels/manage.js deleted file mode 100644 index 8588aa0c..00000000 --- a/client/js/ticket_levels/manage.js +++ /dev/null @@ -1,82 +0,0 @@ -// License: LGPL-3.0-or-later -var request = require('../common/client') -var path = '/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/ticket_levels' -const reorder = require('../components/drag-to-reorder') - -reorder(`${path}/update_order`, 'js-reorderTickets') - -module.exports = index_ticket_levels - -appl.def('ticket_levels', { - show_create_or_edit: function(action, i){ - var reset = {name: '', amount: 0, limit: '', id: '', description: ''} - appl.def('ticket_levels', { - currently_editing: action === 'edit' ? appl.ticket_levels.data[i] : reset, - current_action: action - }) - appl.open_modal('ticketLevelCreateOrEditModal') - }, - create_or_edit: function(form_obj){ - appl.is_loading() - if(appl.ticket_levels.current_action === 'edit') - edit_ticket_level(form_obj) - else - create_ticket_level(form_obj) - }, - delete: function(id){ - request.del(path + '/' + id).end(function(err, resp){ - after_ticket_level_ajax(err, 'delete') - }) - } -}) - - -function index_ticket_levels(path, cb){ - appl.is_loading() - request.get(path).end(function(err, resp) { - appl.def('ticket_levels.data', resp.body.data.map(augment_ticket_level_data)) - if(cb){cb()} - appl.not_loading() - }) - - function augment_ticket_level_data(data) { - if (data.amount === 0) - data.formatted_amount = 'Free' - else - data.formatted_amount = '$' + appl.cents_to_dollars(data.amount) - if (data.limit) - data.remaining = data.limit - data.quantity - if (data.remaining <= 0) - data.sold_out = true - return data - } -} - - - -function edit_ticket_level(form_obj) { - request.put(path + '/' + form_obj.id, form_obj).end(function(err, resp){ - after_ticket_level_ajax(err, 'update') - }) -} - - -function create_ticket_level(form_obj) { - request.post(path, form_obj).end(function(err, resp){ - after_ticket_level_ajax(err, 'create') - }) -} - - -function after_ticket_level_ajax(err, action) { - appl.not_loading() - if(err) - appl.notify("Sorry, we weren't able to " + action + " your ticket. Please try again in a moment.") - else { - appl.notify('Ticket level succesfully ' + action + 'd.') - index_ticket_levels(path) - appl.open_modal('manageTicketLevelsModal') - } -} - -index_ticket_levels(path) diff --git a/client/js/tickets/index/delete-ticket.js b/client/js/tickets/index/delete-ticket.js deleted file mode 100644 index 6dcc6943..00000000 --- a/client/js/tickets/index/delete-ticket.js +++ /dev/null @@ -1,32 +0,0 @@ -// License: LGPL-3.0-or-later -const R = require('ramda') -const flyd = require('flyd') -flyd.flatMap = require('flyd/module/flatmap') -const request = require('../../common/request') -const confirmation = require('../../common/confirmation') - -const stream = flyd.stream() - -var table = document.querySelector('.js-table') -table.addEventListener('click', ev=> { - if(ev.target.hasAttribute('data-remove-ticket')) { - confirmation('Are you sure you want to remove this attendee?', - () => stream(ev.target.getAttribute('data-ticket-id')) - ) - } -}) - -const pathPrefix = `/nonprofits/${app.nonprofit_id}/events/${appl.event_id}/tickets/` - -const response = flyd.flatMap( - ticketID => flyd.map(R.prop('body'), request({method: 'delete', path: pathPrefix + ticketID})).load -, stream ) - -// XXX remove viewscript here -flyd.map( - res => { - appl.notify('Successfully removed that attendee') - appl.tickets.index() - } -, response ) - diff --git a/client/js/tickets/index/page.js b/client/js/tickets/index/page.js deleted file mode 100644 index a3ab00dd..00000000 --- a/client/js/tickets/index/page.js +++ /dev/null @@ -1,195 +0,0 @@ -// License: LGPL-3.0-or-later -require('../../common/restful_resource') -require('../new') -var create_card = require('../../cards/create') -var create_donation = require('../../donations/create') -var request = require('../../common/super-agent-promise') -var get_ticket_levels = require('../../ticket_levels/get_totals') -var format_err = require('../../common/format_response_error') -var format = require('../../common/format') -var confirmation = require('../../common/confirmation') -appl.def('is_usa', format.geography.isUS) -require('../../common/restful_resource') -require('../../components/tables/filtering/apply_filter')('tickets') -require('./delete-ticket') - -function metricsFetch() { - appl.def('loading_metrics', true) - request.get('/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/metrics') - .perform() - .then(function(resp) { - appl - .def('loading_metrics', false) - .def('metrics', resp.body) - }) - appl.def('loading_ticket_levels', true) - get_ticket_levels(app.nonprofit_id, appl.event_id) - .then(function(resp) { - appl.def('loading_ticket_levels', false) - }) -} - -function fetch(query) { - query = query || {page: 1} - query.page = query.page || 1 - appl.def('loading_tickets', true) - return request.get('/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/tickets') - .query(query) - .perform() - .then(function(resp) { - appl.def('loading_tickets', false) - if(query.page > 1) appl.concat('tickets.data', resp.body.data) - else appl.def('tickets', resp.body) - }) -} - - -appl.ticket_wiz.on_complete = function(tickets) { - fetch() - metricsFetch() -} - -appl.def('donations.path_prefix', '/') - -appl.def('tickets.index', function() { - appl.def('appl.tickets.query.page', appl.tickets.query.page || 1) - return fetch(appl.tickets.query) -}) - -appl.def('ajax_donations', { - create: function(form_obj, node) { - appl.def('loading', true) - appl.ajax.create('donations', form_obj, node) - .then(appl.not_loading) - .then(function(resp) { - fetch() - appl.close_modal() - appl.notify("Charge successful") - document.querySelector('.newDonationModal-form').reset() - }) - } -}) - - -appl.def('after_create_card', function(resp) { - fetch() - appl.notify("Card successfully saved!") - location.reload() -}) - -appl.def('tickets', { - path_prefix: '/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/', - query: {page: 1}, - concat_data: true -}) - -appl.def('toggle_checkin', function(id, name, node) { - var checked = appl.prev_elem(node).checked - var message = name + (checked ? ' checked in.' : ' checked out.') - appl.ajax.update('tickets', id, {checked_in: checked}) - .then(function(){ - appl.notify(message) - metricsFetch() - }) -}) - -appl.def('update_ticket', function(id, name, update_text, form_obj) { - appl.ajax.update('tickets', id, form_obj) - .then(function(){ - appl.notify(name + "'s " + update_text + ' updated.') - }) -}) - -appl.def('show_new_donation', function(supporter_id, supporter_name, supporter_email, card_id, card_name) { - appl.def('selected_supporter', { - id: supporter_id, - name: supporter_name, - email: supporter_email - }) - appl.def('selected_card', { - id: card_id, - name: card_name - }) - appl.open_modal('newDonationModal') -}) - -appl.def('show_new_card', function(supporter_id, supporter_name, supporter_email, ticket_id, event_id) { - appl.def('selected_supporter', { - id: supporter_id, - name: supporter_name, - email: supporter_email - }) - appl.def('selected_ticket', { - id: ticket_id - }) - appl.def('selected_event'), { - id: event_id - } - appl.open_modal('newCardModal') -}) - - -// Create a new donation on behalf of a selected supporter and their card -appl.def('create_donation', function(el) { - appl.def('error', '') - appl.def('loading', true) - create_donation(appl.new_donation) - .then(function() { - return fetch() - }) - .then(appl.not_loading) - .then(appl.close_modal) - .then(function() { - appl.prev_elem(el).reset() - appl.notify('Donation successfully made! Receipts have been sent via email.') - location.reload() - }) - .catch(display_err('new_donation_form')) -}) - - -// Create a new card on behalf of a selected supporter -appl.def('create_card', function(card_obj, el) { - appl.def('new_card_form.error', '') - appl.def('loading', true) - create_card({type: 'Supporter', id: appl.selected_supporter.id, email: appl.selected_supporter.email}, card_obj, {event_id: appl.event_id}) - .then(function(card) { - appl.prev_elem(el).reset() - appl.notify("Card successfully saved for " + appl.selected_supporter.name) - return appl.ajax.update('tickets', appl.selected_ticket.id, {token: card.token}) - }) - .then(function() { - return fetch() - }) - .then(appl.not_loading) - .then(appl.close_modal) - .then(() => location.reload()) - .catch(display_err('new_card_form')) -}) - -function display_err(scope) { - return function(resp) { - appl.def('loading', false) - appl.def('error', format_err(resp)) - } -} - -appl.def('remove_card', function(ticket_id, elm) { - var result = confirmation('Are you sure?') - result.confirmed = function() { - appl.is_loading() - - request.post('/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/tickets/' + ticket_id + '/delete_card_for_ticket') - .send({event_id: appl.event_id, ticket_id:ticket_id}) - .perform() - .then(function(resp) { - appl.not_loading() - appl.notify('Successfully deleted card') - appl.tickets.index() - }) - } -}) - - -fetch() -metricsFetch() diff --git a/client/js/tickets/new.js b/client/js/tickets/new.js deleted file mode 100644 index b4b90f82..00000000 --- a/client/js/tickets/new.js +++ /dev/null @@ -1,32 +0,0 @@ -// License: LGPL-3.0-or-later -var path = '/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/ticket_levels' -var indexTicketLevels = require('../ticket_levels/manage') -var formSerialize = require('form-serialize') -var request = require('../common/super-agent-promise') - -require('../components/wizard') -require('./wizard') - -appl.def('show_new_tickets', function(){ - // indexes ticket levels before showing the new ticket modal - // so that ticket level quantites are up-to-date. - // indexTicketLevels takes the path and a callback - indexTicketLevels(path, show_new_modal) -}) - -appl.def('add_ticket_note', function(n) { - var data = formSerialize(appl.prev_elem(n), {hash: true}) - appl.def('loading', true) - request.put('/nonprofits/' + app.nonprofit_id + '/events/' + app.event_id + '/tickets/' + appl.created_ticket_id + '/add_note') - .send({ticket: data}) - .perform() - .then(function(resp) { - appl.def('loading', false) - appl.close_modal() - }) -}) - -function show_new_modal(){ - appl.open_modal('newTicketModal') -} - diff --git a/client/js/tickets/wizard.js b/client/js/tickets/wizard.js deleted file mode 100644 index ddbc2957..00000000 --- a/client/js/tickets/wizard.js +++ /dev/null @@ -1,150 +0,0 @@ -// License: LGPL-3.0-or-later -if(app.autocomplete) { - require('../components/address-autocomplete') -} -require('../cards/create') -var request = require('../common/super-agent-promise') -var create_card = require('../cards/create') -var format_err = require('../common/format_response_error') -var path = '/nonprofits/' + app.nonprofit_id + '/events/' + appl.event_id + '/tickets' - -appl.def('ticket_wiz', { - - // Placeholder for a callback that is evaluated after the tickets are redeemed - on_complete: function() {}, - - // Set all the wizard's default data - set_defaults: function() { - appl.def('ticket_wiz.post_data', { - nonprofit_id: app.nonprofit_id, - tickets: [], - kind: "", - supporter_id: "", - }) - }, - - - // Set/process all the ticket data after submitting the "Tickets" step form - set_tickets: function(form_obj) { - hide_err() - var tickets = [] - var total_amount = 0 - var total_quantity = 0 - for(var key in form_obj.tickets) { - var ticket = form_obj.tickets[key] - ticket.quantity = Number(ticket.quantity) - ticket.amount = Number(ticket.amount) - total_quantity += ticket.quantity - total_amount += ticket.quantity * ticket.amount - if(ticket.quantity > 0) tickets.push({ticket_level_id: ticket.ticket_level_id, quantity: ticket.quantity}) - } - appl.def('ticket_wiz.post_data.tickets', tickets) - - // Calculate total quantity and total charge amount - appl.def('ticket_wiz', { - total_amount: total_amount, - total_quantity: total_quantity - }) - - if(total_amount === 0) { - appl.def('ticket_wiz.post_data.kind', 'free') - } else { - appl.def('ticket_wiz.post_data.kind', 'charge') - } - - if(total_quantity > 0) { - appl.wizard.advance('ticket_wiz') - } else { - appl.notify('Please choose at least one ticket.') - } - }, - - - check_if_any_ticket_levels: function(i, name, node) { - var ticket_level_remainder = appl.ticket_levels.data[i].remaining - var value = appl.prev_elem(node).value - if(value >= ticket_level_remainder) { - appl.notify("There are only " + ticket_level_remainder + " tickets remaining for '" - + name + "'.") - appl.prev_elem(node).value = ticket_level_remainder - } - }, - - - save_supporter: function(form_obj) { - appl.ticket_wiz.save_supporter_promise = request - .post('/nonprofits/' + app.nonprofit_id + '/supporters') - .send({supporter: form_obj}).perform() - .then(function(res) { - appl.ticket_wiz.supporter = res.body - appl.ticket_wiz.post_data.supporter_id = res.body.id - return res.body - }) - .catch(show_err) - appl.wizard.advance('ticket_wiz') - }, - - set_kind: function(node) { - // Tickets creations have a kind of free, offsite, or charge - // OffsitePayments have a kind of check or cash - // We need to save each separately - var op_kind = appl.prev_elem(node).value - var ticket_kind = appl.prev_elem(node).getAttribute('data-ticket-kind') - appl.def('ticket_wiz.post_data.kind', ticket_kind) - appl.def('ticket_wiz.post_data.offsite_payment.kind', op_kind) - }, - - send_payment: function(form_obj) { - appl.def('loading', true) - return appl.ticket_wiz.save_supporter_promise - .then(function(supporter) { - return create_card({type: 'Supporter', id: supporter.id, email: supporter.email}, form_obj) - }) - .catch(show_err) - .then(function(card) { - appl.ticket_wiz.post_data.token = card.token - }) - .then(appl.ticket_wiz.create_tickets) - }, - - create_tickets: function() { - appl.def('loading', true) - return request.post(path) - .send(appl.ticket_wiz.post_data).perform() - .then(complete_wizard) - .then(appl.ticket_wiz.on_complete) - .catch(show_err) - }, - -}) // end appl.def('ticket_wiz'... - - -// To be called when either a free or purchased ticket was successfully -// redeemed; will show a success/thank-you modal -function complete_wizard(resp) { - appl.def('created_ticket_id', resp.body.tickets[0].id) - appl.def('loading', false) - appl.open_modal('confirmTicketsModal') - appl.ticket_wiz.set_defaults() - appl.wizard.reset("ticket_wiz") - hide_err() -} - - -// Display an error on the ticket wizard -// Works on the amount step, supporter step, and free ticket confirmation step. -// The card form step is a special case, it needs some extra state to be set -function show_err(resp) { - appl.def('loading', false) - appl.def('error', format_err(resp)) - appl.def('card_form', {error: true, status: format_err(resp), loading: false, progress_width: '0%'}) -} - -// Hide any errors in the wizard -function hide_err() { - appl.def('loading', false) - appl.def('error', '') - appl.def('card_form', {status: '', error: false, loading: false}) -} - -appl.ticket_wiz.set_defaults() diff --git a/client/js/widget/donate-button.v2.js b/client/js/widget/donate-button.v2.js deleted file mode 100644 index b7d24b2d..00000000 --- a/client/js/widget/donate-button.v2.js +++ /dev/null @@ -1,237 +0,0 @@ -// License: LGPL-3.0-or-later -/* this file expects a config/config.json that contains -{ "button":{ - "url":"https://commitchange.com", - "css":"https://s3-us-west-1.amazonaws.com/commitchange/manual/donate-button.v2.css" - } -} - - this file is generated by rails when compiling the assets -*/ - - -function on_ios11() { - var userAgent = window.navigator.userAgent; - var has11 = userAgent.search("OS 11_\\d") > 0 - var hasMacOS = userAgent.search(" like Mac OS X") > 0 - - return has11 && hasMacOS; -} - -window.commitchange = { - iframes: [] -, modalIframe: null -} - -commitchange.getParamsFromUrl = (whitelist) => { - var result = {}, - tmp = []; - var items = location.search.substr(1).split("&"); - for (var index = 0; index < items.length; index++) { - tmp = items[index].split("="); - if (whitelist.indexOf(tmp[0])) result[tmp[0]] = decodeURIComponent(tmp[1]); - } - return result; -} - -commitchange.openDonationModal = (iframe, overlay) => { - return (event) => { - overlay.className = 'commitchange-overlay commitchange-open' - iframe.className = 'commitchange-iframe commitchange-open' - if (on_ios11()) { - iframe.style.position = 'absolute' - } - commitchange.setParams(commitchange.getParamsFromButton(event.currentTarget), iframe) - if (on_ios11()) { - iframe.scrollIntoView() - } - - commitchange.open_iframe = iframe - commitchange.open_overlay = overlay - } -} - -// Dynamically set the params of the appended iframe donate window -commitchange.setParams = (params, iframe) => { - params.command = 'setDonationParams' - params.sender = 'commitchange' - iframe.contentWindow.postMessage(JSON.stringify(params), fullHost) -} - -commitchange.hideDonation = () => { - if(!commitchange.open_overlay || !commitchange.open_iframe) return - commitchange.open_overlay.className = 'commitchange-overlay commitchange-closed' - commitchange.open_iframe.className = 'commitchange-iframe commitchange-closed' - if (on_ios11()) { - commitchange.open_iframe.style.position = 'fixed' - } - commitchange.open_overlay = undefined - commitchange.open_iframe = undefined -} - -const fullHost = 'REPLACE_FULL_HOST' - -commitchange.overlay = () => { - let div = document.createElement('div') - div.setAttribute('class', 'commitchange-closed commitchange-overlay') - return div -} - -commitchange.createIframe = (source) => { - let i = document.createElement('iframe') - const url = document.location.href - i.setAttribute('class', 'commitchange-closed commitchange-iframe') - i.src = source + "&origin=" + url - return i -} - -// Given a button with a bunch of data parameters -// return an object of key/vals corresponing to each param -commitchange.getParamsFromButton = (elem) => { - let options = { - offsite: 't' - , type: elem.getAttribute('data-type') - , custom_amounts: elem.getAttribute('data-custom-amounts') || elem.getAttribute('data-amounts') - , amount: elem.getAttribute('data-amount') - , minimal: elem.getAttribute('data-minimal') - , weekly: elem.getAttribute('data-weekly') - , default: elem.getAttribute('data-default') - , custom_fields: elem.getAttribute('data-custom-fields') - , campaign_id: elem.getAttribute('data-campaign-id') - , gift_option_id: elem.getAttribute('data-gift-option-id') - , redirect: elem.getAttribute('data-redirect') - , designation: elem.getAttribute('data-designation') - , multiple_designations: elem.getAttribute('data-multiple-designations') - , hide_dedication: elem.getAttribute('data-hide-dedication')? true : false - , designations_prompt: elem.getAttribute('data-designations-prompt') - , single_amount: elem.getAttribute('data-single-amount') - , designation_desc: elem.getAttribute('data-designation-desc') || elem.getAttribute('data-description') - , locale: elem.getAttribute('data-locale') - , "utm_source": elem.getAttribute('data-utm_source') - , "utm_campaign": elem.getAttribute('data-utm_campaign') - , "utm_medium": elem.getAttribute('data-utm_medium') - , "utm_content": elem.getAttribute('data-utm_content') - , "first_name": elem.getAttribute('data-first_name') - , "last_name": elem.getAttribute('data-last_name') - , "country": elem.getAttribute('data-country') - , "postal_code": elem.getAttribute('data-postal_code') - - - } - // Remove false values from the options - for(let key in options) { - if(!options[key]) delete options[key] - } - return options -} - -commitchange.appendMarkup = () => { - if(commitchange.alreadyAppended) return - else commitchange.alreadyAppended = true - let script = document.getElementById('commitchange-donation-script') || document.getElementById('commitchange-script') - const nonprofitID = script.getAttribute('data-npo-id') - const baseSource = fullHost + "/nonprofits/" + nonprofitID + "/donate?offsite=t" - let elems = document.querySelectorAll('.commitchange-donate') - - for(let i = 0; i < elems.length; ++i) { - let elem = elems[i] - let source = baseSource - - let optionsButton = commitchange.getParamsFromButton(elem) - let options = commitchange.getParamsFromUrl(["utm_campaign","utm_content","utm_source","utm_medium","first_name","last_name","country","postal_code","address","city"]) - for (var attr in optionsButton) { options[attr] = optionsButton[attr]; } - let params = [] - for(let key in options) { - params.push(key + '=' + options[key]) - } - source += "&" + params.join("&") - - if(elem.hasAttribute('data-embedded')) { - source += '&mode=embedded' - let iframe = commitchange.createIframe(source) - elem.appendChild(iframe) - iframe.setAttribute('class', 'commitchange-iframe-embedded') - commitchange.iframes.push(iframe) - } else { - // Show the CommitChange-branded button if it's not set to custom. - if(!elem.hasAttribute('data-custom') && !elem.hasAttribute('data-custom-button')) { - let btn_iframe = document.createElement('iframe') - let btn_src = fullHost + "/nonprofits/" + nonprofitID + "/btn" - if(elem.hasAttribute('data-fixed')) { btn_src += '?fixed=t' } - btn_iframe.src = btn_src - btn_iframe.className = 'commitchange-btn-iframe' - btn_iframe.setAttribute('scrolling', 'no') - btn_iframe.setAttribute('seamless', 'seamless') - elem.appendChild(btn_iframe) - btn_iframe.onclick = commitchange.openDonationModal(iframe, overlay) - } - // Create the iframe overlay for this button - let modal = document.createElement('div') - modal.className = 'commitchange-modal' - let overlay = commitchange.overlay() - let iframe - if(commitchange.modalIframe) { - iframe = commitchange.modalIframe - } else { - iframe = commitchange.createIframe(source) - commitchange.iframes.push(iframe) - commitchange.modalIframe = iframe - } - modal.appendChild(overlay) - document.body.appendChild(iframe) - elem.parentNode.appendChild(modal) - overlay.onclick = commitchange.hideDonation - elem.onclick = commitchange.openDonationModal(iframe, overlay) - } // end else - } // end for loop -} - -// Load the CSS for the parent page element from our AWS server -commitchange.loadStylesheet = () => { - if(commitchange.alreadyStyled) return - else commitchange.alreadyStyled = true - let stylesheet = document.createElement('link') - stylesheet.href = "REPLACE_CSS_URL" - stylesheet.rel = 'stylesheet' - stylesheet.type = 'text/css' - document.getElementsByTagName('head')[0].appendChild(stylesheet) -} - - -// Handle iframe post messages -if(window.addEventListener) { - window.addEventListener('message', (e) => { - // Close the modal - if(e.data === 'commitchange:close') { - commitchange.hideDonation() - } - // Redirect on donation completion using the redirect param - else if(e.data.match(/^commitchange:redirect/)) { - const matches = e.data.match(/^commitchange:redirect:(.+)$/) - if(matches.length === 2) window.location.href = matches[1] - } - }) -} - -// Make initialization calls on document load -if(document.addEventListener) { - document.addEventListener("DOMContentLoaded", (event) => { - commitchange.loadStylesheet() - commitchange.appendMarkup() - }) -} else if(window.jQuery) { - window.jQuery(document).ready(() => { - commitchange.loadStylesheet() - commitchange.appendMarkup() - }) -} else { - window.onload = () => { - commitchange.loadStylesheet() - commitchange.appendMarkup() - } -} - -if(document.querySelector('.commitchange-donate')) { - commitchange.loadStylesheet() - commitchange.appendMarkup() -} From 8c9dc3af37368b11ba702ec3393d74249149df3a Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 19 May 2020 14:57:08 -0500 Subject: [PATCH 333/440] Correct javascript ordering issue in nonprofits/show view --- app/views/nonprofits/show.html.erb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/nonprofits/show.html.erb b/app/views/nonprofits/show.html.erb index 27aab095..17041b6c 100755 --- a/app/views/nonprofits/show.html.erb +++ b/app/views/nonprofits/show.html.erb @@ -20,6 +20,7 @@ <% end %> <%= content_for :javascripts do %> + <%= javascript_pack_tag 'i18n', 'page__nonprofits__show', 'page__nonprofits__edit' %> <%= render 'schema', campaign: @campaign, url: @url %> @@ -45,13 +45,13 @@ <%= content_for :facebook_tags do %> - + "> <% end %> <%= content_for :twitter_tags do %> - + "> <% end %> <% if current_campaign_editor? %> diff --git a/app/views/events/_edit_form.html.erb b/app/views/events/_edit_form.html.erb index 3c3eb9da..a0d7548e 100644 --- a/app/views/events/_edit_form.html.erb +++ b/app/views/events/_edit_form.html.erb @@ -84,7 +84,7 @@

Used for sharing on social media

-
+
")'> Edit
diff --git a/app/views/events/show.html.erb b/app/views/events/show.html.erb index 91c555f9..8f7cd92c 100644 --- a/app/views/events/show.html.erb +++ b/app/views/events/show.html.erb @@ -9,13 +9,13 @@ <%= content_for :facebook_tags do %> - + " /> <% end %> <%= content_for :twitter_tags do %> - + " /> <% end %> <%= content_for :javascripts do %> diff --git a/app/views/events/stats.html.erb b/app/views/events/stats.html.erb index 025bb382..755f260e 100644 --- a/app/views/events/stats.html.erb +++ b/app/views/events/stats.html.erb @@ -8,13 +8,13 @@ <%= content_for :facebook_tags do %> - + " /> <% end %> <%= content_for :twitter_tags do %> - + " /> <% end %> <%= content_for :javascripts do %> diff --git a/app/views/layouts/_admin_menu.html.erb b/app/views/layouts/_admin_menu.html.erb index 3f04c1b4..548f194b 100644 --- a/app/views/layouts/_admin_menu.html.erb +++ b/app/views/layouts/_admin_menu.html.erb @@ -2,7 +2,7 @@ <% if current_role?([:nonprofit_admin,:nonprofit_associate]) %>
- + <%= image_tag administered_nonprofit.logo_by_size(:small), class:"sideNav-profile" %> <%= administered_nonprofit.name %> diff --git a/app/views/layouts/_user_menu.html.erb b/app/views/layouts/_user_menu.html.erb index 1f8bb3b3..b6ec1308 100644 --- a/app/views/layouts/_user_menu.html.erb +++ b/app/views/layouts/_user_menu.html.erb @@ -3,8 +3,8 @@
- <% if current_user.profile.picture? %> - + <% if current_user.profile.picture.attached? %> + <%= image_tag current_user.profile.picture_by_size(:tiny), class: 'sideNav-profile' %> <% else %> <% end %> diff --git a/app/views/nonprofits/_edit.html.erb b/app/views/nonprofits/_edit.html.erb index 0132e14b..08a8343a 100644 --- a/app/views/nonprofits/_edit.html.erb +++ b/app/views/nonprofits/_edit.html.erb @@ -98,7 +98,7 @@
-
+
")'> Upload
diff --git a/app/views/nonprofits/_header_content.html.erb b/app/views/nonprofits/_header_content.html.erb index 9514d4cf..c45298b3 100644 --- a/app/views/nonprofits/_header_content.html.erb +++ b/app/views/nonprofits/_header_content.html.erb @@ -3,7 +3,7 @@
- diff --git a/app/views/nonprofits/_overview_media.html.erb b/app/views/nonprofits/_overview_media.html.erb index 02172a7d..28437a5f 100644 --- a/app/views/nonprofits/_overview_media.html.erb +++ b/app/views/nonprofits/_overview_media.html.erb @@ -1,11 +1,11 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%>
- <% if @nonprofit.main_image.file %> + <% if @nonprofit.main_image.attached? %>
- <%= image_tag @nonprofit.main_image_url(:nonprofit_carousel).to_s %> + <%= image_tag @nonprofit.main_image_by_size(:nonprofit_carousel) %>
diff --git a/app/views/nonprofits/_settings_modals.html.erb b/app/views/nonprofits/_settings_modals.html.erb index 752a50ff..1660a9ad 100644 --- a/app/views/nonprofits/_settings_modals.html.erb +++ b/app/views/nonprofits/_settings_modals.html.erb @@ -23,7 +23,7 @@

-
+
")'> Select
diff --git a/app/views/nonprofits/donate.html.erb b/app/views/nonprofits/donate.html.erb index 7ec3f2de..4cfc807e 100755 --- a/app/views/nonprofits/donate.html.erb +++ b/app/views/nonprofits/donate.html.erb @@ -33,13 +33,13 @@ <%= content_for :facebook_tags do %> - + <% end %> <%= content_for :twitter_tags do %> - + " /> <% end %> <% content_for :body_id do %>donate<% end %> diff --git a/app/views/nonprofits/email/_footer.html.erb b/app/views/nonprofits/email/_footer.html.erb index 95f12a86..3ea56a39 100644 --- a/app/views/nonprofits/email/_footer.html.erb +++ b/app/views/nonprofits/email/_footer.html.erb @@ -28,9 +28,9 @@ - <% if @nonprofit.logo_url %> + <% if @nonprofit.logo.attached? %> <% end %> diff --git a/app/views/nonprofits/show.html.erb b/app/views/nonprofits/show.html.erb index 41dd817e..87f5f881 100755 --- a/app/views/nonprofits/show.html.erb +++ b/app/views/nonprofits/show.html.erb @@ -7,12 +7,12 @@ <%= content_for :facebook_tags do %> - + " /> <% end %> <%= content_for :twitter_tags do %> - + " /> <% end %> <%= content_for :stylesheets do %> diff --git a/app/views/nonprofits/supporter_form.html.erb b/app/views/nonprofits/supporter_form.html.erb index c217d2db..dbe7ddbd 100644 --- a/app/views/nonprofits/supporter_form.html.erb +++ b/app/views/nonprofits/supporter_form.html.erb @@ -16,7 +16,9 @@
- <%= image_tag @nonprofit.logo_url(:normal).to_s %> + <% if @nonprofit.logo.attached? %> + <%= image_tag url_for(@nonprofit.logo_by_size(:normal)) %> + <% end %> <% if params[:title] %>

<%= params[:title] %>

<% else %> diff --git a/app/views/profiles/show.html.erb b/app/views/profiles/show.html.erb index 486fe7b7..9be77c1f 100644 --- a/app/views/profiles/show.html.erb +++ b/app/views/profiles/show.html.erb @@ -14,7 +14,7 @@
- + <%= image_tag @nonprofit.logo_by_size(:normal), style:'border: 1px solid rgba(0,0,0,0.05);'%>
- + <% supporter = (@profile.supporters && @profile.supporters.length != 0) ? supporter : false %> From 34882dc099ffc77fd6913edbd36a0b326b0a9e96 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 18 May 2020 16:05:25 -0500 Subject: [PATCH 401/440] Make additional updates to ActiveStorage --- app/controllers/campaigns_controller.rb | 2 +- app/controllers/events_controller.rb | 4 ++-- lib/create/create_peer_to_peer_campaign.rb | 6 +++--- lib/fetch/fetch_background_image.rb | 8 -------- lib/insert/insert_duplicate.rb | 8 ++++---- 5 files changed, 10 insertions(+), 18 deletions(-) delete mode 100644 lib/fetch/fetch_background_image.rb diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index 177ec176..bb931ac3 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -51,7 +51,7 @@ class CampaignsController < ApplicationController @peer_to_peer_campaign_param = @campaign.id end - @campaign_background_image = FetchBackgroundImage.with_model(@campaign) + @campaign_background_image = @campaign.background_image.attached? && url_for(@campaign.background_image_by_size(:normal)) end def activities diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index b3522da3..21d2e7a3 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -19,7 +19,7 @@ class EventsController < ApplicationController def show @event = params[:event_slug] ? Event.find_by_slug!(params[:event_slug]) : Event.find_by_id!(params[:id]) - @event_background_image = FetchBackgroundImage.with_model(@event) + @event_background_image = @event.background_image.attached? && url_for(@event.background_image_by_size(:normal)) @nonprofit = @event.nonprofit if @event.deleted && !current_event_editor? redirect_to nonprofit_path(current_nonprofit) @@ -71,7 +71,7 @@ class EventsController < ApplicationController def stats @event = current_event @url = Format::Url.concat(root_url, @event.url) - @event_background_image = FetchBackgroundImage.with_model(@event) + @event_background_image = @event.background_image.attached? && url_for(@event.background_image_by_size(:normal)) render layout: 'layouts/embed' end diff --git a/lib/create/create_peer_to_peer_campaign.rb b/lib/create/create_peer_to_peer_campaign.rb index 91d997ca..f7ac746f 100644 --- a/lib/create/create_peer_to_peer_campaign.rb +++ b/lib/create/create_peer_to_peer_campaign.rb @@ -24,17 +24,17 @@ module CreatePeerToPeerCampaign campaign.save begin - campaign.update_attribute(:main_image, parent_campaign.main_image) if parent_campaign.main_image + campaign.main_image.attach(parent_campaign.main_image.blob) if parent_campaign.main_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end begin - campaign.update_attribute(:background_image, parent_campaign.background_image) if parent_campaign.background_image + campaign.background_image.attach(parent_campaign.background_image.blob) if parent_campaign.background_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end begin - campaign.update_attribute(:banner_image, parent_campaign.banner_image) if parent_campaign.banner_image + campaign.banner_image.attach(parent_campaign.banner_image.blob) if parent_campaign.banner_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end diff --git a/lib/fetch/fetch_background_image.rb b/lib/fetch/fetch_background_image.rb deleted file mode 100644 index 5c3a7e72..00000000 --- a/lib/fetch/fetch_background_image.rb +++ /dev/null @@ -1,8 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module FetchBackgroundImage - def self.with_model(model) - return model.background_image_url(:normal) unless model.background_image.file.nil? - end -end diff --git a/lib/insert/insert_duplicate.rb b/lib/insert/insert_duplicate.rb index d0c68f07..3fa26af0 100644 --- a/lib/insert/insert_duplicate.rb +++ b/lib/insert/insert_duplicate.rb @@ -30,13 +30,13 @@ module InsertDuplicate dupe.save! begin - dupe.update_attribute(:main_image, campaign.main_image) if campaign.main_image + dupe.main_image.attach(campaign.main_image.blob) if campaign.main_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end begin - dupe.update_attribute(:background_image, campaign.background_image) if campaign.background_image + dupe.background_image.attach(campaign.background_image.blob) if campaign.background_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end @@ -84,13 +84,13 @@ module InsertDuplicate dupe.save! begin - dupe.update_attribute(:main_image, event.main_image) if event.main_image + dupe.main_image.attach(event.main_image.blob) if event.main_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end begin - dupe.update_attribute(:background_image, event.background_image) if event.background_image + dupe.background_image.attach( event.background_image.blob) if event.background_image.attached? rescue StandardError AWS::S3::Errors::NoSuchKey end From 54c4b766931b9f6d24825c512f987024f8e8c63e Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 19 May 2020 14:16:53 -0500 Subject: [PATCH 402/440] Add Active Storage types --- package.json | 1 + tsconfig.json | 2 +- types/rails__activestorage/index.d.ts | 3 +++ yarn.lock | 5 +++++ 4 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 types/rails__activestorage/index.d.ts diff --git a/package.json b/package.json index 998dc3c0..19e8dd2c 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ }, "devDependencies": { "@babel/preset-env": "^7.7.1", + "@types/activestorage": "^5.2.2", "@types/color": "^3.0.0", "@types/enzyme": "^3.1.9", "@types/enzyme-to-json": "^1.5.1", diff --git a/tsconfig.json b/tsconfig.json index 678bb30f..99893565 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -10,7 +10,7 @@ "experimentalDecorators": true, "baseUrl": "./", "lib": ["dom","es5", "scripthost", "es2015.promise"], - "paths": { "*": [ "./types/*"] }, + "paths": { "*": [ "./types/*"], "@rails/activestorage": ["./types/rails__activestorage"] }, "typeRoots" : [ "node_modules/@types", "./types"] diff --git a/types/rails__activestorage/index.d.ts b/types/rails__activestorage/index.d.ts new file mode 100644 index 00000000..0315f3a5 --- /dev/null +++ b/types/rails__activestorage/index.d.ts @@ -0,0 +1,3 @@ +import {start, DirectUpload, DirectUploadDelegate, Blob} from 'activestorage'; + +export {start, DirectUpload, DirectUploadDelegate, Blob}; \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index eea7b66a..bebb673e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1021,6 +1021,11 @@ resolved "https://registry.yarnpkg.com/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz#8da5c6530915653f3a1f38fd5f101d8c3f8079c5" integrity sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ== +"@types/activestorage@^5.2.2": + version "5.2.2" + resolved "https://registry.yarnpkg.com/@types/activestorage/-/activestorage-5.2.2.tgz#82473e47c25f3e1488400dca69b982a1591dbac0" + integrity sha512-qsyspA6wVRz8tAN4lYaPXrlcZ1AcqbQyBfNNg/zEmQbMu5/ch8f2rspGLK+A4ZhgoRXG0/ivSPeZ/a+1g10U4w== + "@types/babel__core@^7.1.0": version "7.1.7" resolved "https://registry.yarnpkg.com/@types/babel__core/-/babel__core-7.1.7.tgz#1dacad8840364a57c98d0dd4855c6dd3752c6b89" From d0393fa2d796e2dc0d15a76d56b95e24b81e4aac Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 20 May 2020 15:36:31 -0500 Subject: [PATCH 403/440] Upgrade to Rails 6.0.3 --- Gemfile | 9 +- Gemfile.lock | 134 ++++++++++-------- bin/setup | 15 +- config/application.rb | 2 + config/environments/development.rb | 38 ++--- config/environments/production.rb | 57 +++++--- config/environments/test.rb | 20 +-- .../initializers/content_security_policy.rb | 32 ++++- .../new_framework_defaults_6_0.rb | 45 ++++++ config/locales/en.yml | 98 +++++-------- config/puma.rb | 49 ++++--- config/storage.yml | 39 ++++- 12 files changed, 319 insertions(+), 219 deletions(-) create mode 100644 config/initializers/new_framework_defaults_6_0.rb diff --git a/Gemfile b/Gemfile index 190eec19..01371c1c 100755 --- a/Gemfile +++ b/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' ruby '2.6.6' -gem 'rails', '~> 5.2.3' +gem 'rails', '~> 6.0.3' gem 'jbuilder', '~> 2.10' gem 'bootsnap', '~> 1.4', require: false # Large rails application booting enhancer gem 'font_assets', '~> 0.1.14' # for serving fonts on cdn https://github.com/ericallam/font_assets @@ -40,7 +40,7 @@ gem 'geocoder', '~> 1.5' # for adding latitude and longitude to location-based t gem 'i18n-js', '~> 3.3' gem 'lograge', '~> 0.11.2' # make logging less terrible in rails gem 'nearest_time_zone', '~> 0.0.4' # for detecting timezone from lat/lng https://github.com/buytruckload/nearest_time_zone -gem 'rails-i18n', '~> 5.1', '>= 5.1.3' +gem 'rails-i18n', '~> 6', '~> 6.0.0' gem 'roadie-rails', '~> 2.1' # email generation helpers gem 'table_print', '~> 1.5', '>= 1.5.6' # Nice table printing of data for the console @@ -58,12 +58,12 @@ gem 'image_processing', '~> 1.10.3' # User authentication # https://github.com/plataformatec/devise gem 'devise-async', '~> 1.0' -gem 'devise', '~> 4.4' +gem 'devise', '~> 4.7' # API Tools gem 'config', '> 1.5' gem 'dry-validation', '~> 0.13.3' # used only for config validation -gem 'foreman', '~> 0.85.0' +gem 'foreman', '~> 0.87.1' gem 'wisper', '~> 2.0' gem 'wisper-activejob', '~> 1.0.0' @@ -87,6 +87,7 @@ group :development, :ci, :test do gem 'parallel_tests', '~> 2.32' gem 'factory_bot_rails', '~> 5.0', '>= 5.0.2' gem 'factory_bot', '~> 5.0', '>= 5.0.2' + gem 'listen' end group :ci, :test do diff --git a/Gemfile.lock b/Gemfile.lock index 1ea0c400..c0485926 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -25,52 +25,65 @@ GEM remote: https://rubygems.org/ specs: action_mailer_matchers (1.2.0) - actioncable (5.2.3) - actionpack (= 5.2.3) + actioncable (6.0.3.1) + actionpack (= 6.0.3.1) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailer (5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) + actionmailbox (6.0.3.1) + actionpack (= 6.0.3.1) + activejob (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) + mail (>= 2.7.1) + actionmailer (6.0.3.1) + actionpack (= 6.0.3.1) + actionview (= 6.0.3.1) + activejob (= 6.0.3.1) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (5.2.3) - actionview (= 5.2.3) - activesupport (= 5.2.3) - rack (~> 2.0) + actionpack (6.0.3.1) + actionview (= 6.0.3.1) + activesupport (= 6.0.3.1) + rack (~> 2.0, >= 2.0.8) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.2) - actionview (5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (~> 1.0, >= 1.2.0) + actiontext (6.0.3.1) + actionpack (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) + nokogiri (>= 1.8.5) + actionview (6.0.3.1) + activesupport (= 6.0.3.1) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) - rails-html-sanitizer (~> 1.0, >= 1.0.3) - activejob (5.2.3) - activesupport (= 5.2.3) + rails-html-sanitizer (~> 1.1, >= 1.2.0) + activejob (6.0.3.1) + activesupport (= 6.0.3.1) globalid (>= 0.3.6) - activemodel (5.2.3) - activesupport (= 5.2.3) - activerecord (5.2.3) - activemodel (= 5.2.3) - activesupport (= 5.2.3) - arel (>= 9.0) - activestorage (5.2.3) - actionpack (= 5.2.3) - activerecord (= 5.2.3) + activemodel (6.0.3.1) + activesupport (= 6.0.3.1) + activerecord (6.0.3.1) + activemodel (= 6.0.3.1) + activesupport (= 6.0.3.1) + activestorage (6.0.3.1) + actionpack (= 6.0.3.1) + activejob (= 6.0.3.1) + activerecord (= 6.0.3.1) marcel (~> 0.3.1) - activesupport (5.2.3) + activesupport (6.0.3.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) tzinfo (~> 1.1) + zeitwerk (~> 2.2, >= 2.2.2) addressable (2.6.0) public_suffix (>= 2.0.2, < 4.0) amq-protocol (2.3.0) andand (1.3.3) - arel (9.0.0) ast (2.4.0) aws-sdk (1.67.0) aws-sdk-v1 (= 1.67.0) @@ -115,10 +128,10 @@ GEM debase-ruby_core_source (0.10.5) debug_inspector (0.0.3) deep_merge (1.2.1) - devise (4.6.2) + devise (4.7.1) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 4.1.0, < 6.0) + railties (>= 4.1.0) responders warden (~> 1.2.3) devise-async (1.0.0) @@ -174,8 +187,7 @@ GEM ffi (1.11.1) font_assets (0.1.14) rack - foreman (0.85.0) - thor (~> 0.19.1) + foreman (0.87.1) fullcontact (0.18.0) faraday (~> 0.11.0) faraday_middleware (>= 0.10) @@ -209,6 +221,9 @@ GEM activesupport (>= 5.0.0) json (1.8.6) kdtree (0.4) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) lograge (0.11.2) actionpack (>= 4) activesupport (>= 4) @@ -227,11 +242,11 @@ GEM mime-types (3.2.2) mime-types-data (~> 3.2015) mime-types-data (3.2019.0331) - mimemagic (0.3.3) + mimemagic (0.3.5) mini_magick (4.10.1) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.14.0) + minitest (5.14.1) msgpack (1.3.1) multi_json (1.13.1) multi_xml (0.6.0) @@ -241,7 +256,7 @@ GEM kdtree require_all netrc (0.11.0) - nio4r (2.4.0) + nio4r (2.5.2) nokogiri (1.10.9) mini_portile2 (~> 2.4.0) orm_adapter (0.5.0) @@ -276,35 +291,40 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.5.1) - rails (5.2.3) - actioncable (= 5.2.3) - actionmailer (= 5.2.3) - actionpack (= 5.2.3) - actionview (= 5.2.3) - activejob (= 5.2.3) - activemodel (= 5.2.3) - activerecord (= 5.2.3) - activestorage (= 5.2.3) - activesupport (= 5.2.3) + rails (6.0.3.1) + actioncable (= 6.0.3.1) + actionmailbox (= 6.0.3.1) + actionmailer (= 6.0.3.1) + actionpack (= 6.0.3.1) + actiontext (= 6.0.3.1) + actionview (= 6.0.3.1) + activejob (= 6.0.3.1) + activemodel (= 6.0.3.1) + activerecord (= 6.0.3.1) + activestorage (= 6.0.3.1) + activesupport (= 6.0.3.1) bundler (>= 1.3.0) - railties (= 5.2.3) + railties (= 6.0.3.1) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.3.0) loofah (~> 2.3) - rails-i18n (5.1.3) + rails-i18n (6.0.0) i18n (>= 0.7, < 2) - railties (>= 5.0, < 6) - railties (5.2.3) - actionpack (= 5.2.3) - activesupport (= 5.2.3) + railties (>= 6.0.0, < 7) + railties (6.0.3.1) + actionpack (= 6.0.3.1) + activesupport (= 6.0.3.1) method_source rake (>= 0.8.7) - thor (>= 0.19.0, < 2.0) + thor (>= 0.20.3, < 2.0) rainbow (3.0.0) rake (12.3.3) + rb-fsevent (0.10.4) + rb-inotify (0.10.1) + ffi (~> 1.0) request_store (1.4.1) rack (>= 1.4) require_all (2.0.0) @@ -389,7 +409,7 @@ GEM table_print (1.5.6) test-unit (3.3.3) power_assert - thor (0.19.4) + thor (1.0.1) thread_safe (0.3.6) tilt (2.0.9) timecop (0.9.1) @@ -424,6 +444,7 @@ GEM wisper wisper-rspec (1.1.0) xml-simple (1.1.5) + zeitwerk (2.3.0) PLATFORMS ruby @@ -442,7 +463,7 @@ DEPENDENCIES countries (~> 3.0) database_cleaner (~> 1.7) debase (~> 0.2.3) - devise (~> 4.4) + devise (~> 4.7) devise-async (~> 1.0) dotenv-rails (~> 2.7, >= 2.7.5) dry-validation (~> 0.13.3) @@ -450,7 +471,7 @@ DEPENDENCIES factory_bot_rails (~> 5.0, >= 5.0.2) ffi (~> 1.11, >= 1.11.1) font_assets (~> 0.1.14) - foreman (~> 0.85.0) + foreman (~> 0.87.1) fullcontact (~> 0.18.0) geocoder (~> 1.5) hamster (~> 3.0) @@ -459,6 +480,7 @@ DEPENDENCIES i18n-js (~> 3.3) image_processing (~> 1.10.3) jbuilder (~> 2.10) + listen lograge (~> 0.11.2) mail_view (~> 2.0) mini_magick (~> 4.10.1) @@ -476,8 +498,8 @@ DEPENDENCIES rack-attack (~> 5.2) rack-ssl (~> 1.4) rack-timeout (~> 0.5.1) - rails (~> 5.2.3) - rails-i18n (~> 5.1, >= 5.1.3) + rails (~> 6.0.3) + rails-i18n (~> 6.0.0, ~> 6) rake (~> 12.3.2) roadie-rails (~> 2.1) rspec (~> 3.8) diff --git a/bin/setup b/bin/setup index 31400462..0e39e8cb 100755 --- a/bin/setup +++ b/bin/setup @@ -1,19 +1,16 @@ #!/usr/bin/env ruby -# frozen_string_literal: true - -require 'pathname' require 'fileutils' -include FileUtils # path to your application root. -APP_ROOT = Pathname.new File.expand_path('..', __dir__) +APP_ROOT = File.expand_path('..', __dir__) def system!(*args) system(*args) || abort("\n== Command #{args} failed ==") end -chdir APP_ROOT do - # This script is a starting point to setup your application. +FileUtils.chdir APP_ROOT do + # This script is a way to setup or update your development environment automatically. + # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. puts '== Installing dependencies ==' @@ -22,11 +19,11 @@ chdir APP_ROOT do # puts "\n== Copying sample files ==" # unless File.exist?('config/database.yml') - # cp 'config/database.yml.sample', 'config/database.yml' + # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' # end puts "\n== Preparing database ==" - system! 'bin/rails db:setup' + system! 'bin/rails db:prepare' puts "\n== Removing old logs and tempfiles ==" system! 'bin/rails log:clear tmp:clear' diff --git a/config/application.rb b/config/application.rb index d8b3d045..ee4c5499 100755 --- a/config/application.rb +++ b/config/application.rb @@ -11,6 +11,8 @@ require "active_record/railtie" require "active_storage/engine" require "action_controller/railtie" require "action_mailer/railtie" +# require "action_mailbox/engine" +# require "action_text/engine" require "action_view/railtie" # require "action_cable/engine" # require "sprockets/railtie" diff --git a/config/environments/development.rb b/config/environments/development.rb index 8a813c28..22399e99 100755 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -17,12 +17,14 @@ Rails.application.configure do config.consider_all_requests_local = true # Enable/disable caching. By default caching is disabled. - if Rails.root.join('tmp/caching-dev.txt').exist? + # Run rails dev:cache to toggle caching. + if Rails.root.join('tmp', 'caching-dev.txt').exist? config.action_controller.perform_caching = true + config.action_controller.enable_fragment_cache_logging = true config.cache_store = :memory_store config.public_file_server.headers = { - 'Cache-Control' => 'public, max-age=172800' + 'Cache-Control' => "public, max-age=#{2.days.to_i}" } else config.action_controller.perform_caching = false @@ -30,15 +32,8 @@ Rails.application.configure do config.cache_store = :null_store end - # You can uncomment the following to test our real AWS email server on localhost: - # config.action_mailer.delivery_method = :aws_ses - # config.action_mailer.default_url_options = { host: 'commitchange.com' } - config.action_mailer.delivery_method = Settings.mailer.delivery_method.to_sym - config.action_mailer.smtp_settings = { address: Settings.mailer.address, port: Settings.mailer.port } - config.action_mailer.smtp_settings['user_name'] = Settings.mailer.username if Settings.mailer.username - config.action_mailer.smtp_settings['password'] = Settings.mailer.password if Settings.mailer.password - - config.action_mailer.default_url_options = { host: Settings.mailer.host } + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local # Don't care if the mailer can't send. config.action_mailer.raise_delivery_errors = false @@ -48,12 +43,12 @@ Rails.application.configure do # Print deprecation notices to the Rails logger. config.active_support.deprecation = :log - # Raise exception on mass assignment protection for Active Record models - # config.active_record.mass_assignment_sanitizer = :strict - # Raise an error on page load if there are pending migrations. config.active_record.migration_error = :page_load + # Highlight code that triggered database queries in logs. + config.active_record.verbose_query_logs = true + # Debug mode disables concatenation and preprocessing of assets. # This option may cause significant delays in view rendering with a large # number of complex assets. @@ -62,27 +57,16 @@ Rails.application.configure do # Suppress logger output for asset requests. config.assets.quiet = true - # Adds additional error checking when serving assets at runtime. - # Checks for improperly declared sprockets dependencies. - # Raises helpful error messages. - config.assets.raise_runtime_errors = true - - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true # 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.file_watcher = ActiveSupport::EventedFileUpdateChecker config.log_level = :debug config.dependency_loading = true if $rails_rake_task config.middleware.use I18n::JS::Middleware - - # SASS Helpers - config.sass.inline_source_maps = true - config.sass.line_comments = false - - config.active_storage.service = :local end diff --git a/config/environments/production.rb b/config/environments/production.rb index 821517b3..62beede6 100755 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -1,5 +1,3 @@ -# frozen_string_literal: true - Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. @@ -21,12 +19,15 @@ Rails.application.configure do # For large-scale production use, consider using a caching reverse proxy like nginx, varnish or squid. # config.action_dispatch.rack_cache = true - # Disable serving static files from thne `/public` folder by default since + # Ensures that a master key has been made available in either ENV["RAILS_MASTER_KEY"] + # or in config/master.key. This key is used to decrypt credentials (and other encrypted files). + # config.require_master_key = true + + # Disable serving static files from the `/public` folder by default since # Apache or NGINX already handles this. config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present? - # Compress JavaScripts and CSS. - config.assets.js_compressor = :uglifier + # Compress CSS using a preprocessor. # config.assets.css_compressor = :sass # Do not fallback to assets pipeline if a precompiled asset is missed. @@ -34,31 +35,32 @@ Rails.application.configure do # Generate digests for assets URLs. config.assets.digest = true - - # `config.assets.precompile` and `config.assets.version` have moved to config/initializers/assets.rb - # Enable serving of images, stylesheets, and JavaScripts from an asset server. # config.action_controller.asset_host = 'http://assets.example.com' # Specifies the header that your server uses for sending files. - # config.action_dispatch.x_sendfile_header = "X-Sendfile" # for Apache - # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for Nginx + # config.action_dispatch.x_sendfile_header = 'X-Sendfile' # for Apache + # config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for NGINX + + # Store uploaded files on the local file system (see config/storage.yml for options). + config.active_storage.service = :local # Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies. config.force_ssl = true - # Set to :debug to see everything in the log. - config.log_level = :info + # Use the lowest log level to ensure availability of diagnostic information + # when problems arise. + config.log_level = :debug # Prepend all log lines with the following tags. - config.log_tags = [:request_id] + config.log_tags = [ :request_id ] # Use a different cache store in production. # config.cache_store = :mem_cache_store - # Use a real queuing backend for Active Job (and separate queues per environment) + # Use a real queuing backend for Active Job (and separate queues per environment). # config.active_job.queue_adapter = :resque - # config.active_job.queue_name_prefix = "commitchange_#{Rails.env}" + # config.active_job.queue_name_prefix = "commitchange_production" config.action_mailer.perform_caching = false @@ -80,12 +82,33 @@ Rails.application.configure do # require 'syslog/logger' # config.logger = ActiveSupport::TaggedLogging.new(Syslog::Logger.new 'app-name') - if ENV['RAILS_LOG_TO_STDOUT'].present? + if ENV["RAILS_LOG_TO_STDOUT"].present? logger = ActiveSupport::Logger.new(STDOUT) logger.formatter = config.log_formatter - config.logger = ActiveSupport::TaggedLogging.new(logger) + config.logger = ActiveSupport::TaggedLogging.new(logger) end # Do not dump schema after migrations. config.active_record.dump_schema_after_migration = false + + # Inserts middleware to perform automatic connection switching. + # The `database_selector` hash is used to pass options to the DatabaseSelector + # middleware. The `delay` is used to determine how long to wait after a write + # to send a subsequent read to the primary. + # + # The `database_resolver` class is used by the middleware to determine which + # database is appropriate to use based on the time delay. + # + # The `database_resolver_context` class is used by the middleware to set + # timestamps for the last write to the primary. The resolver uses the context + # class timestamps to determine how long to wait before reading from the + # replica. + # + # By default Rails will store a last write timestamp in the session. The + # DatabaseSelector middleware is designed as such you can define your own + # strategy for connection switching and pass that into the middleware through + # these configuration options. + # config.active_record.database_selector = { delay: 2.seconds } + # config.active_record.database_resolver = ActiveRecord::Middleware::DatabaseSelector::Resolver + # config.active_record.database_resolver_context = ActiveRecord::Middleware::DatabaseSelector::Resolver::Session end diff --git a/config/environments/test.rb b/config/environments/test.rb index f15baa69..aedcdfb4 100755 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -1,12 +1,11 @@ -# frozen_string_literal: true +# The test environment is used exclusively to run your application's +# test suite. You never need to work with it otherwise. Remember that +# your test database is "scratch space" for the test suite and is wiped +# and recreated between test runs. Don't rely on the data there! -Commitchange::Application.configure do +Rails.application.configure do # Settings specified here will take precedence over those in config/application.rb. - # The test environment is used exclusively to run your application's - # test suite. You never need to work with it otherwise. Remember that - # your test database is "scratch space" for the test suite and is wiped - # and recreated between test runs. Don't rely on the data there! config.cache_classes = true # Do not eager load code on boot. This avoids loading your whole application @@ -17,12 +16,13 @@ Commitchange::Application.configure do # Configure public file server for tests with Cache-Control for performance. config.public_file_server.enabled = true config.public_file_server.headers = { - 'Cache-Control' => 'public, max-age=3600' + 'Cache-Control' => "public, max-age=#{1.hour.to_i}" } # Show full error reports and disable caching. config.consider_all_requests_local = true config.action_controller.perform_caching = false + config.cache_store = :null_store # Raise exceptions instead of rendering exception templates. config.action_dispatch.show_exceptions = false @@ -30,6 +30,9 @@ Commitchange::Application.configure do # Disable request forgery protection in test environment. config.action_controller.allow_forgery_protection = false + # Store uploaded files on the local file system in a temporary directory. + config.active_storage.service = :test + config.action_mailer.perform_caching = false # Tell Action Mailer not to deliver emails to the real world. @@ -43,11 +46,10 @@ Commitchange::Application.configure do config.action_mailer.default_url_options = { host: 'houdiniproject.test' } - # Raises error for missing translations + # Raises error for missing translations. # config.action_view.raise_on_missing_translations = true #recommended by https://github.com/grosser/parallel_tests/wiki config.cache_store = :file_store, Rails.root.join("tmp", "cache", "paralleltests#{ENV['TEST_ENV_NUMBER']}") - config.active_storage.service = :test end diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb index ab00184c..41c43016 100644 --- a/config/initializers/content_security_policy.rb +++ b/config/initializers/content_security_policy.rb @@ -1,6 +1,28 @@ -# frozen_string_literal: true +# Be sure to restart your server when you modify this file. -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -Rails.application.config.content_security_policy do |policy| - policy.connect_src :self, :https, "http://localhost:3035", "ws://localhost:3035" if Rails.env.development? -end \ No newline at end of file +# Define an application-wide content security policy +# For further information see the following documentation +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy + +# Rails.application.config.content_security_policy do |policy| +# policy.default_src :self, :https +# policy.font_src :self, :https, :data +# policy.img_src :self, :https, :data +# policy.object_src :none +# policy.script_src :self, :https +# policy.style_src :self, :https + +# # Specify URI for violation reports +# # policy.report_uri "/csp-violation-report-endpoint" +# end + +# If you are using UJS then enable automatic nonce generation +# Rails.application.config.content_security_policy_nonce_generator = -> request { SecureRandom.base64(16) } + +# Set the nonce only to specific directives +# Rails.application.config.content_security_policy_nonce_directives = %w(script-src) + +# Report CSP violations to a specified URI +# For further information see the following documentation: +# https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only +# Rails.application.config.content_security_policy_report_only = true diff --git a/config/initializers/new_framework_defaults_6_0.rb b/config/initializers/new_framework_defaults_6_0.rb new file mode 100644 index 00000000..92240ef5 --- /dev/null +++ b/config/initializers/new_framework_defaults_6_0.rb @@ -0,0 +1,45 @@ +# Be sure to restart your server when you modify this file. +# +# This file contains migration options to ease your Rails 6.0 upgrade. +# +# Once upgraded flip defaults one by one to migrate to the new default. +# +# Read the Guide for Upgrading Ruby on Rails for more info on each option. + +# Don't force requests from old versions of IE to be UTF-8 encoded. +# Rails.application.config.action_view.default_enforce_utf8 = false + +# Embed purpose and expiry metadata inside signed and encrypted +# cookies for increased security. +# +# This option is not backwards compatible with earlier Rails versions. +# It's best enabled when your entire app is migrated and stable on 6.0. +# Rails.application.config.action_dispatch.use_cookies_with_metadata = true + +# Change the return value of `ActionDispatch::Response#content_type` to Content-Type header without modification. +# Rails.application.config.action_dispatch.return_only_media_type_on_content_type = false + +# Return false instead of self when enqueuing is aborted from a callback. +# Rails.application.config.active_job.return_false_on_aborted_enqueue = true + +# Send Active Storage analysis and purge jobs to dedicated queues. +# Rails.application.config.active_storage.queues.analysis = :active_storage_analysis +# Rails.application.config.active_storage.queues.purge = :active_storage_purge + +# When assigning to a collection of attachments declared via `has_many_attached`, replace existing +# attachments instead of appending. Use #attach to add new attachments without replacing existing ones. +# Rails.application.config.active_storage.replace_on_assign_to_many = true + +# Use ActionMailer::MailDeliveryJob for sending parameterized and normal mail. +# +# The default delivery jobs (ActionMailer::Parameterized::DeliveryJob, ActionMailer::DeliveryJob), +# will be removed in Rails 6.1. This setting is not backwards compatible with earlier Rails versions. +# If you send mail in the background, job workers need to have a copy of +# MailDeliveryJob to ensure all delivery jobs are processed properly. +# Make sure your entire app is migrated and stable on 6.0 before using this setting. +# Rails.application.config.action_mailer.delivery_job = "ActionMailer::MailDeliveryJob" + +# Enable the same cache key to be reused when the object being cached of type +# `ActiveRecord::Relation` changes by moving the volatile information (max updated at and count) +# of the relation's cache key into the cache version to support recycling cache key. +# Rails.application.config.active_record.collection_cache_versioning = true diff --git a/config/locales/en.yml b/config/locales/en.yml index a745be9f..bd8814c5 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,6 +1,33 @@ -# License: CC0-1.0 -# Sample localization file for English. Add more files in this directory for other locales. -# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points. +# Files in the config/locales directory are used for internationalization +# and are automatically loaded by Rails. If you want to use locales other +# than English, add the necessary files in this directory. +# +# To use the locales, use `I18n.t`: +# +# I18n.t 'hello' +# +# In views, this is aliased to just `t`: +# +# <%= t('hello') %> +# +# To use a different locale, set it with `I18n.locale`: +# +# I18n.locale = :es +# +# This would use the information in config/locales/es.yml. +# +# The following keys must be escaped otherwise they will not be retrieved by +# the default I18n backend: +# +# true, false, on, off, yes, no +# +# Instead, surround them with single quotes. +# +# en: +# 'true': 'foo' +# +# To learn more, please read the Rails Internationalization guide +# available at https://guides.rubyonrails.org/i18n.html. en: hello: "Hello world" @@ -10,8 +37,6 @@ en: body: 'Comment content' organization: name: "Organisation" - organization_page: - promote: "Promote this organization" donation: amount: "Total Amount" date: "Transaction Date" @@ -29,9 +54,8 @@ en: subject: "Donation receipt for %{nonprofit_name}" transfer_info_html: "This transfer will appear on your bank statement as %{label}" transfer_label_html: "Donation %{nonprofit_statement}." - oneoff_donation_html: "Your donation towards %{nonprofit_name} was successful!" - recurring_donation_html: "Your recurring donation towards %{nonprofit_name}, started on %{start_date}, has been successfully paid." - recurring_donation_cancel_modify_html: "If you need to update your card or cancel your recurring donation, please follow this link: %{management_url}" + oneoff_donation_html: "Thank you for your donation to %{nonprofit_name} and for joining thousands of people across Europe who are invested in making our movement a true force to be reckoned with. Your support will go towards ensuring we can move fast to win the campaigns that matter to all of us." + recurring_donation_html: "Thank you for your regular donation to %{nonprofit_name} and for joining thousands of people across Europe who are invested in making our movement a true force to be reckoned with. Your support will go towards ensuring we can move fast to win the campaigns that matter to all of us." donor_direct_debit_notification: subject: "Donation receipt for %{nonprofit_name}" transfer_info_html: "This transfer will appear on your bank statement as %{label}" @@ -142,61 +166,3 @@ en: twitter: "Tweet" twitter_message: "Join me in supporting" finish: "Finish" - registration: - get_started: - header: "Get started" - description: "Let's get started with Houdini. To begin, fill out your initial nonprofit and info." - wizard: - tabs: - nonprofit: "Nonprofit" - contact: "Contact" - nonprofit: - name: - label: "Organization Name" - placeholder: "Ending Poverty in the Fox Valley Inc." - website: - label: "Website URL" - placeholder: "http://www.endpovertyinthefoxvalleyinc.org" - email: - label: "Org Email (public)" - placeholder: "contact@endpovertyinthefoxvalleyinc.org" - phone: - label: "Org Phone (public)" - placeholder: "(555) 555-5555" - city: - label: "City" - placeholder: "Appleton" - state: - label: "State" - placeholder: "WI" - zip: - label: "Zip Code" - placeholder: "54915" - contact: - name: - label: "Your Name" - placeholder: "Penelope Schultz" - email: - label: "Your Email (used for login)" - placeholder: "penelope@endpovertyinthefoxvalleyinc.org" - password: - label: "New Password" - password_confirmation: - label: "Retype Password" - phone: - label: "Your Phone (for account recovery)" - placeholder: "(555) 555-5555" - save_and_finish: "Save & Finish" - saving: "Saving..." - next: "Next" - footer: - terms_and_privacy: "Terms & Privacy" - about: "About" - login: - header: "Login" - email: "Email" - password: "Password" - login: "Login" - logging_in: "Logging you in..." - forgot_password: "Forgot Password?" - get_started: "Get Started" diff --git a/config/puma.rb b/config/puma.rb index f3eaa09e..b3e7e5c6 100644 --- a/config/puma.rb +++ b/config/puma.rb @@ -3,31 +3,40 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Puma can serve each request in a thread from an internal thread pool. -# The `threads` method setting takes two numbers a minimum and maximum. +# The `threads` method setting takes two numbers: a minimum and maximum. # Any libraries that use thread pools should be configured to match # the maximum value specified for Puma. Default is set to 5 threads for minimum -# and maximum, this matches the default thread size of Active Record. +# and maximum; this matches the default thread size of Active Record. # -threads_count = ENV.fetch('RAILS_MAX_THREADS') { 5 }.to_i -threads threads_count, threads_count +max_threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 } +min_threads_count = ENV.fetch("RAILS_MIN_THREADS") { max_threads_count } +threads min_threads_count, max_threads_count +# Specifies the `port` that Puma will listen on to receive requests; default is 3000. +# +port ENV.fetch("PORT") { 5000 } + +# Specifies the `environment` that Puma will run in. +# +environment ENV.fetch("RAILS_ENV") { "development" } + +# Specifies the `pidfile` that Puma will use. +pidfile ENV.fetch("PIDFILE") { "tmp/pids/server.pid" } + +# Specifies the number of `workers` to boot in clustered mode. +# Workers are forked web server processes. If using threads and workers together +# the concurrency of the application would be max `threads` * `workers`. +# Workers do not work on JRuby or Windows (both of which do not support +# processes). +# + workers ENV.fetch("WEB_CONCURRENCY") { 2 } if ENV['RAILS_ENV'] != 'development' + +# Use the `preload_app!` method when specifying a `workers` number. +# This directive tells Puma to first boot the application and load code +# before forking the application. This takes advantage of Copy On Write +# process behavior so workers use less memory. +# preload_app! if ENV['RAILS_ENV'] != 'development' -rackup DefaultRackup -port ENV.fetch('PORT') { 5000 } -environment ENV.fetch('RAILS_ENV') { 'development' } - -workers Integer(ENV['WEB_CONCURRENCY'] || 1) - -on_worker_boot do - # ActiveSupport.on_load(:active_record) do - # config = ActiveRecord::Base.configurations[Rails.env] || - # Rails.application.config.database_configuration[Rails.env] - # config['pool'] = ENV['RAILS_MAX_THREADS'] || 1 - # ActiveRecord::Base.establish_connection - # end - ActiveRecord::Base.establish_connection if defined?(ActiveRecord) -end - # Allow puma to be restarted by `rails restart` command. plugin :tmp_restart diff --git a/config/storage.yml b/config/storage.yml index ed10455d..d32f76e8 100644 --- a/config/storage.yml +++ b/config/storage.yml @@ -1,7 +1,34 @@ -local: - service: Disk - root: <%= Rails.root.join("storage") %> - test: - service: Disk - root: <%= Rails.root.join("tmp/storage") %> \ No newline at end of file + service: Disk + root: <%= Rails.root.join("tmp/storage") %> + +local: + service: Disk + root: <%= Rails.root.join("storage") %> + +# Use rails credentials:edit to set the AWS secrets (as aws:access_key_id|secret_access_key) +# amazon: +# service: S3 +# access_key_id: <%= Rails.application.credentials.dig(:aws, :access_key_id) %> +# secret_access_key: <%= Rails.application.credentials.dig(:aws, :secret_access_key) %> +# region: us-east-1 +# bucket: your_own_bucket + +# Remember not to checkin your GCS keyfile to a repository +# google: +# service: GCS +# project: your_project +# credentials: <%= Rails.root.join("path/to/gcs.keyfile") %> +# bucket: your_own_bucket + +# Use rails credentials:edit to set the Azure Storage secret (as azure_storage:storage_access_key) +# microsoft: +# service: AzureStorage +# storage_account_name: your_account_name +# storage_access_key: <%= Rails.application.credentials.dig(:azure_storage, :storage_access_key) %> +# container: your_container_name + +# mirror: +# service: Mirror +# primary: local +# mirrors: [ amazon, google, microsoft ] From 0248fb56639df0b9821fa69c53f64db34cf40450 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 20 May 2020 15:56:52 -0500 Subject: [PATCH 404/440] Update rails in houdini_upgrade.gemspec --- gems/houdini_upgrade/houdini_upgrade.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gems/houdini_upgrade/houdini_upgrade.gemspec b/gems/houdini_upgrade/houdini_upgrade.gemspec index e418deec..9584880f 100644 --- a/gems/houdini_upgrade/houdini_upgrade.gemspec +++ b/gems/houdini_upgrade/houdini_upgrade.gemspec @@ -20,5 +20,5 @@ Gem::Specification.new do |spec| spec.files = Dir["{app,config,db,lib}/**/*", "LICENSE", "AGPL-3.0.txt", "GPL-3.0.txt", "LGPL-3.0.txt", "Rakefile", "README.md"] - spec.add_dependency "rails", "~> 5.2.3" + spec.add_dependency "rails", "~> 6.0.3" end From 7ccd6870aaf23a9881b7d05b5c41eaf0ac5770a1 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 20 May 2020 16:03:16 -0500 Subject: [PATCH 405/440] Handle rails 6 deprecation of update_attributes to update --- app/controllers/campaigns_controller.rb | 2 +- app/controllers/events_controller.rb | 2 +- app/controllers/nonprofits/payments_controller.rb | 2 +- app/controllers/nonprofits_controller.rb | 2 +- app/controllers/profiles_controller.rb | 2 +- app/controllers/ticket_levels_controller.rb | 2 +- app/controllers/tickets_controller.rb | 2 +- app/controllers/users/confirmations_controller.rb | 2 +- app/controllers/users/registrations_controller.rb | 2 +- lib/cancel_billing_subscription.rb | 2 +- lib/create/create_campaign.rb | 2 +- lib/create/create_custom_field_join.rb | 2 +- lib/pay_recurring_donation.rb | 2 +- lib/update/update_campaign_gift_option.rb | 2 +- lib/update/update_recurring_donations.rb | 4 ++-- lib/update/update_supporter.rb | 2 +- 16 files changed, 17 insertions(+), 17 deletions(-) diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index bb931ac3..10f89278 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -67,7 +67,7 @@ class CampaignsController < ApplicationController Time.use_zone(current_nonprofit.timezone || 'UTC') do campaign_params[:end_datetime] = Chronic.parse(campaign_params[:end_datetime]) if campaign_params[:end_datetime].present? end - current_campaign.update_attributes campaign_params + current_campaign.update campaign_params json_saved current_campaign, 'Successfully updated!' end diff --git a/app/controllers/events_controller.rb b/app/controllers/events_controller.rb index 21d2e7a3..33d13e4c 100644 --- a/app/controllers/events_controller.rb +++ b/app/controllers/events_controller.rb @@ -46,7 +46,7 @@ class EventsController < ApplicationController event_params[:start_datetime] = Chronic.parse(event_params[:start_datetime]) if event_params[:start_datetime].present? event_params[:end_datetime] = Chronic.parse(event_params[:end_datetime]) if event_params[:end_datetime].present? end - current_event.update_attributes(event_params) + current_event.update(event_params) json_saved current_event, 'Successfully updated' end diff --git a/app/controllers/nonprofits/payments_controller.rb b/app/controllers/nonprofits/payments_controller.rb index cae2eddf..8bea6257 100644 --- a/app/controllers/nonprofits/payments_controller.rb +++ b/app/controllers/nonprofits/payments_controller.rb @@ -46,7 +46,7 @@ module Nonprofits def update @payment = current_nonprofit.payments.find(params[:id]) - @payment.update_attributes(payment_params) + @payment.update(payment_params) json_saved @payment end diff --git a/app/controllers/nonprofits_controller.rb b/app/controllers/nonprofits_controller.rb index f38d0db8..d54af193 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -57,7 +57,7 @@ class NonprofitsController < ApplicationController def update flash[:notice] = 'Update successful!' - current_nonprofit.update_attributes nonprofit_params.except(:verification_status) + current_nonprofit.update nonprofit_params.except(:verification_status) json_saved current_nonprofit end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index 0fcfacfb..72e7b7eb 100755 --- a/app/controllers/profiles_controller.rb +++ b/app/controllers/profiles_controller.rb @@ -47,7 +47,7 @@ class ProfilesController < ApplicationController else current_user.profile end - @profile.update_attributes(profile_params) + @profile.update(profile_params) json_saved @profile, 'Profile updated' end diff --git a/app/controllers/ticket_levels_controller.rb b/app/controllers/ticket_levels_controller.rb index ab6e7dcb..a59cef10 100644 --- a/app/controllers/ticket_levels_controller.rb +++ b/app/controllers/ticket_levels_controller.rb @@ -22,7 +22,7 @@ class TicketLevelsController < ApplicationController end def update - current_ticket_level.update_attributes ticket_level_params + current_ticket_level.update ticket_level_params json_saved current_ticket_level, 'Ticket level updated' end diff --git a/app/controllers/tickets_controller.rb b/app/controllers/tickets_controller.rb index 21ed41ec..72fdc605 100644 --- a/app/controllers/tickets_controller.rb +++ b/app/controllers/tickets_controller.rb @@ -48,7 +48,7 @@ class TicketsController < ApplicationController # PUT nonprofits/:nonprofit_id/events/:event_id/tickets/:id/add_note def add_note - current_nonprofit.tickets.find(params[:id]).update_attributes(note: ticket_params[:note]) + current_nonprofit.tickets.find(params[:id]).update(note: ticket_params[:note]) render json: {} end diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb index 77601e37..2ff745f8 100644 --- a/app/controllers/users/confirmations_controller.rb +++ b/app/controllers/users/confirmations_controller.rb @@ -25,7 +25,7 @@ class Users::ConfirmationsController < Devise::ConfirmationsController def confirm @user = User.find(params[:id]) - if @user.valid? && @user.update_attributes(params[:user].except(:confirmation_token)) + if @user.valid? && @user.update(params[:user].except(:confirmation_token)) flash[:notice] = 'Your account is all set!' sign_in @user redirect_to session[:donor_signup_url] || root_url diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index c697853d..2476e519 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -31,7 +31,7 @@ class Users::RegistrationsController < Devise::RegistrationsController if current_user.pending_password && params[:user][:password] && params[:user][:password_confirmation] params[:user][:pending_password] = false end - success = current_user.update_attributes(params[:user]) + success = current_user.update(params[:user]) errs = current_user.errors.full_messages else success = false diff --git a/lib/cancel_billing_subscription.rb b/lib/cancel_billing_subscription.rb index ef33f55c..1590fbcc 100644 --- a/lib/cancel_billing_subscription.rb +++ b/lib/cancel_billing_subscription.rb @@ -25,7 +25,7 @@ module CancelBillingSubscription end billing_plan_id = Settings.default_bp.id - billing_subscription.update_attributes( + billing_subscription.update( billing_plan_id: billing_plan_id, status: 'active' ) diff --git a/lib/create/create_campaign.rb b/lib/create/create_campaign.rb index 207e4cd0..29c47196 100644 --- a/lib/create/create_campaign.rb +++ b/lib/create/create_campaign.rb @@ -19,7 +19,7 @@ module CreateCampaign # json_saved campaign, 'Campaign created! Well done.' else profile_id = params[:campaign][:profile_id] - Profile.find(profile_id).update_attributes params[:profile] + Profile.find(profile_id).update params[:profile] return CreatePeerToPeerCampaign.create(params[:campaign], profile_id) end end diff --git a/lib/create/create_custom_field_join.rb b/lib/create/create_custom_field_join.rb index 6f4e42ce..62818e6b 100644 --- a/lib/create/create_custom_field_join.rb +++ b/lib/create/create_custom_field_join.rb @@ -22,7 +22,7 @@ module CreateCustomFieldJoin custom_fields.each do |custom_field| existing = supporter.custom_field_joins.find_by_custom_field_master_id(custom_field[:custom_field_master_id]) if existing - existing.update_attributes( + existing.update( custom_field_master_id: custom_field[:custom_field_master_id], value: custom_field[:value] ) diff --git a/lib/pay_recurring_donation.rb b/lib/pay_recurring_donation.rb index 655b0512..c454a1e2 100644 --- a/lib/pay_recurring_donation.rb +++ b/lib/pay_recurring_donation.rb @@ -75,7 +75,7 @@ module PayRecurringDonation 'old_donation' => true )) if result['charge']['status'] != 'failed' - rd.update_attributes(n_failures: 0) + rd.update(n_failures: 0) result['recurring_donation'] = rd HoudiniEventPublisher.announce(:recurring_donation_payment_succeeded, donation, donation&.supporter&.locale || 'en') InsertActivities.for_recurring_donations([result['payment']['id']]) diff --git a/lib/update/update_campaign_gift_option.rb b/lib/update/update_campaign_gift_option.rb index 5163f2dc..73ce1ac4 100644 --- a/lib/update/update_campaign_gift_option.rb +++ b/lib/update/update_campaign_gift_option.rb @@ -3,7 +3,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateCampaignGiftOption def self.update(gift_option, params) - gift_option.update_attributes params + gift_option.update params gift_option end end diff --git a/lib/update/update_recurring_donations.rb b/lib/update/update_recurring_donations.rb index c5932a3e..7ade0e09 100644 --- a/lib/update/update_recurring_donations.rb +++ b/lib/update/update_recurring_donations.rb @@ -105,13 +105,13 @@ module UpdateRecurringDonations def self.update(rd, params) params = set_defaults(params) if params[:donation] - rd.donation.update_attributes(params[:donation]) + rd.donation.update(params[:donation]) return rd.donation unless rd.donation.valid? params = params.except(:donation) end - rd.update_attributes(params) + rd.update(params) rd end diff --git a/lib/update/update_supporter.rb b/lib/update/update_supporter.rb index 37addd8e..9b3e356f 100644 --- a/lib/update/update_supporter.rb +++ b/lib/update/update_supporter.rb @@ -3,7 +3,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later module UpdateSupporter def self.from_info(supporter, params) - supporter.update_attributes(params) + supporter.update(params) # GeocodeModel.delay.geocode(supporter) supporter end From 1d672381c7181659cf2d10e1330a037cb577231d Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 20 May 2020 16:40:20 -0500 Subject: [PATCH 406/440] Upgrade spec gems --- Gemfile | 6 +++--- Gemfile.lock | 49 ++++++++++++++++++++++++++----------------------- 2 files changed, 29 insertions(+), 26 deletions(-) diff --git a/Gemfile b/Gemfile index 01371c1c..1488dd85 100755 --- a/Gemfile +++ b/Gemfile @@ -82,8 +82,8 @@ group :development, :ci, :test do gem 'pry-byebug', '~> 3.7.0' gem 'ruby-prof', '0.15.9' gem 'standard', '~> 0.1.2' - gem 'rspec-rails', '~> 3.8', '>= 3.8.2' - gem 'rspec', '~> 3.8' + gem 'rspec-rails', '~> 4.0.0' + gem 'rspec', '~> 3.9.0' gem 'parallel_tests', '~> 2.32' gem 'factory_bot_rails', '~> 5.0', '>= 5.0.2' gem 'factory_bot', '~> 5.0', '>= 5.0.2' @@ -92,7 +92,7 @@ end group :ci, :test do gem 'action_mailer_matchers', '~> 1.2' - gem 'database_cleaner', '~> 1.7' + gem 'database_cleaner-active_record' gem 'simplecov', '~> 0.16.1', require: false gem 'stripe-ruby-mock', '~> 2.4.1', require: 'stripe_mock', git: 'https://github.com/commitchange/stripe-ruby-mock.git', branch: '2.4.1' gem 'test-unit', '~> 3.3' diff --git a/Gemfile.lock b/Gemfile.lock index c0485926..27f60791 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -122,7 +122,10 @@ GEM css_parser (1.7.0) addressable dante (0.2.0) - database_cleaner (1.7.0) + database_cleaner (1.8.5) + database_cleaner-active_record (1.8.0) + activerecord + database_cleaner (~> 1.8.0) debase (0.2.4) debase-ruby_core_source (>= 0.10.2) debase-ruby_core_source (0.10.5) @@ -341,27 +344,27 @@ GEM roadie-rails (2.1.0) railties (>= 5.1, < 6.1) roadie (~> 3.1) - rspec (3.8.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-core (3.8.2) - rspec-support (~> 3.8.0) - rspec-expectations (3.8.4) + rspec (3.9.0) + rspec-core (~> 3.9.0) + rspec-expectations (~> 3.9.0) + rspec-mocks (~> 3.9.0) + rspec-core (3.9.2) + rspec-support (~> 3.9.3) + rspec-expectations (3.9.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-mocks (3.8.1) + rspec-support (~> 3.9.0) + rspec-mocks (3.9.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.8.0) - rspec-rails (3.8.2) - actionpack (>= 3.0) - activesupport (>= 3.0) - railties (>= 3.0) - rspec-core (~> 3.8.0) - rspec-expectations (~> 3.8.0) - rspec-mocks (~> 3.8.0) - rspec-support (~> 3.8.0) - rspec-support (3.8.2) + rspec-support (~> 3.9.0) + rspec-rails (4.0.1) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.9) + rspec-expectations (~> 3.9) + rspec-mocks (~> 3.9) + rspec-support (~> 3.9) + rspec-support (3.9.3) rubocop (0.72.0) jaro_winkler (~> 1.5.1) parallel (~> 1.10) @@ -461,7 +464,7 @@ DEPENDENCIES colorize (~> 0.8.1) config (> 1.5) countries (~> 3.0) - database_cleaner (~> 1.7) + database_cleaner-active_record debase (~> 0.2.3) devise (~> 4.7) devise-async (~> 1.0) @@ -502,8 +505,8 @@ DEPENDENCIES rails-i18n (~> 6.0.0, ~> 6) rake (~> 12.3.2) roadie-rails (~> 2.1) - rspec (~> 3.8) - rspec-rails (~> 3.8, >= 3.8.2) + rspec (~> 3.9.0) + rspec-rails (~> 4.0.0) ruby-debug-ide (~> 0.7.0) ruby-prof (= 0.15.9) sassc (~> 2.0, >= 2.0.1) From c727f615ebd700b123a316502d93f07c8589b11e Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 20 May 2020 17:05:06 -0500 Subject: [PATCH 407/440] Fix bug in spec/mailers/admin_spec.rb --- spec/mailers/admin_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/mailers/admin_spec.rb b/spec/mailers/admin_spec.rb index b6f130f9..ac25b390 100644 --- a/spec/mailers/admin_spec.rb +++ b/spec/mailers/admin_spec.rb @@ -11,7 +11,7 @@ RSpec.describe AdminMailer, type: :mailer do let!(:donation) { force_create(:donation, nonprofit_id: np.id, supporter_id: s.id, card_id: oldcard.id, amount: 999) } let!(:charge) { create(:charge, donation: donation, nonprofit: np, amount: 100, created_at: Time.now) } let(:campaign) { force_create(:campaign, nonprofit: np) } - let!(:campaign_gift_option_with_desc) { force_create(:campaign_gift_option, description: 'desc', amount_one_time: ``, campaign: campaign) } + let!(:campaign_gift_option_with_desc) { force_create(:campaign_gift_option, description: 'desc', amount_one_time: 999, campaign: campaign) } let!(:campaign_gift_option) { force_create(:campaign_gift_option, campaign: campaign) } let(:mail) { AdminMailer.notify_failed_gift(donation, campaign_gift_option) } let(:mail_with_desc) { AdminMailer.notify_failed_gift(donation, campaign_gift_option_with_desc) } From 6156e67d468b43a2660f39b039f1443bc7b20879 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 21 May 2020 13:51:34 -0500 Subject: [PATCH 408/440] Update geocoder --- Gemfile | 2 +- Gemfile.lock | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 1488dd85..1e897b6f 100755 --- a/Gemfile +++ b/Gemfile @@ -36,7 +36,7 @@ gem 'fullcontact', '~> 0.18.0' # Full Contact API; includes #Hashie::Mash gem 'chronic', '~> 0.10.2' # For nat lang parsing of dates gem 'colorize', '~> 0.8.1' # Print colorized text in debugger/console gem 'countries', '~> 3.0' -gem 'geocoder', '~> 1.5' # for adding latitude and longitude to location-based tables http://www.rubygeocoder.com/ +gem 'geocoder', '~> 1.6.3' # for adding latitude and longitude to location-based tables http://www.rubygeocoder.com/ gem 'i18n-js', '~> 3.3' gem 'lograge', '~> 0.11.2' # make logging less terrible in rails gem 'nearest_time_zone', '~> 0.0.4' # for detecting timezone from lat/lng https://github.com/buytruckload/nearest_time_zone diff --git a/Gemfile.lock b/Gemfile.lock index 27f60791..ec5f9b76 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -195,7 +195,7 @@ GEM faraday (~> 0.11.0) faraday_middleware (>= 0.10) hashie (>= 2.0, < 4.0) - geocoder (1.5.1) + geocoder (1.6.3) get_process_mem (0.2.4) ffi (~> 1.0) globalid (0.4.2) @@ -476,7 +476,7 @@ DEPENDENCIES font_assets (~> 0.1.14) foreman (~> 0.87.1) fullcontact (~> 0.18.0) - geocoder (~> 1.5) + geocoder (~> 1.6.3) hamster (~> 3.0) heroku-deflater (~> 0.6.3) httparty (~> 0.17.0) From 26be1858d387a0eb732ac47b4994501f943ec2ad Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 21 May 2020 13:52:24 -0500 Subject: [PATCH 409/440] Update Campaign creation to use jbuilder --- app/controllers/campaigns_controller.rb | 8 +++++- app/views/campaigns/_create.jbuilder | 25 +++++++++++++++++++ lib/create/create_campaign.rb | 6 +---- lib/create/create_peer_to_peer_campaign.rb | 4 +-- .../create_peer_to_peer_campaign_spec.rb | 21 ++++++++-------- 5 files changed, 45 insertions(+), 19 deletions(-) create mode 100644 app/views/campaigns/_create.jbuilder diff --git a/app/controllers/campaigns_controller.rb b/app/controllers/campaigns_controller.rb index 10f89278..74773232 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -60,7 +60,13 @@ class CampaignsController < ApplicationController end def create - render json: CreateCampaign.create(params, current_nonprofit) + @campaign = CreateCampaign.create(params, current_nonprofit) + if (@campaign.errors.empty?) + render 'campaigns/create', campaign: @campaign + else + render json: { errors: @campaign.errors.messages }.as_json + end + end def update diff --git a/app/views/campaigns/_create.jbuilder b/app/views/campaigns/_create.jbuilder new file mode 100644 index 00000000..a887badd --- /dev/null +++ b/app/views/campaigns/_create.jbuilder @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.extract! campaign, :id, :name, #basics + :nonprofit_id, :profile_id, :parent_campaign_id # references + :reason_for_supporting, :default_reason_for_supporting, + :published, :deleted + + +json.url campaign_url(nonprofit) + +if campaign.main_image.attached? + json.main_image do + json.full url_for(campaign.main_image) + json.normal url_for(campaign.main_image_by_size(:normal)) + json.thumb url_for(campaign.main_image_by_size(:thumb)) + end +end + +if campaign.background_image.attached? + json.background_image do + json.full url_for(campaign.background_image) + json.normal url_for(campaign.background_image_by_size(:normal)) + end +end \ No newline at end of file diff --git a/lib/create/create_campaign.rb b/lib/create/create_campaign.rb index 29c47196..45131ff8 100644 --- a/lib/create/create_campaign.rb +++ b/lib/create/create_campaign.rb @@ -12,11 +12,7 @@ module CreateCampaign if !params[:campaign][:parent_campaign_id] campaign = nonprofit.campaigns.create params[:campaign] - - return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? - - return campaign.as_json - # json_saved campaign, 'Campaign created! Well done.' + return campaign else profile_id = params[:campaign][:profile_id] Profile.find(profile_id).update params[:profile] diff --git a/lib/create/create_peer_to_peer_campaign.rb b/lib/create/create_peer_to_peer_campaign.rb index f7ac746f..7fe01ee3 100644 --- a/lib/create/create_peer_to_peer_campaign.rb +++ b/lib/create/create_peer_to_peer_campaign.rb @@ -39,8 +39,6 @@ module CreatePeerToPeerCampaign AWS::S3::Errors::NoSuchKey end - return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? - - campaign.as_json['campaign'] + campaign end end diff --git a/spec/lib/create/create_peer_to_peer_campaign_spec.rb b/spec/lib/create/create_peer_to_peer_campaign_spec.rb index 4ab262a7..a53b5f9f 100644 --- a/spec/lib/create/create_peer_to_peer_campaign_spec.rb +++ b/spec/lib/create/create_peer_to_peer_campaign_spec.rb @@ -9,12 +9,12 @@ describe CreatePeerToPeerCampaign do let!(:parent_campaign) { force_create(:campaign, name: 'Parent campaign', nonprofit: force_create(:nm_justice)) } context 'on success' do - it 'returns a hash' do + it 'returns a campaign' do campaign_params = { name: 'Child campaign', parent_campaign_id: parent_campaign.id, goal_amount_dollars: '1000' } Timecop.freeze(2020, 4, 5) do result = CreatePeerToPeerCampaign.create(campaign_params, profile.id) - expect(result).to be_kind_of Hash + expect(result).to be_kind_of Campaign end end @@ -23,9 +23,10 @@ describe CreatePeerToPeerCampaign do Timecop.freeze(2020, 4, 5) do result = CreatePeerToPeerCampaign.create(campaign_params, profile.id) - expect(result).not_to include 'errors' - expect(result['parent_campaign_id']).to eq parent_campaign.id - expect(result['created_at']).to eq 'Sun, 05 Apr 2020 00:00:00 UTC +00:00' + expect(result).to be_kind_of Campaign + expect(result.errors.empty?).to be true + expect(result.parent_campaign_id).to eq parent_campaign.id + expect(result.created_at).to eq 'Sun, 05 Apr 2020 00:00:00 UTC +00:00' end end @@ -34,8 +35,8 @@ describe CreatePeerToPeerCampaign do Timecop.freeze(2020, 4, 5) do result = CreatePeerToPeerCampaign.create(campaign_params, profile.id) - expect(result).not_to include 'errors' - expect(result['slug']).to eq 'child-campaign_000' + expect(result.errors.empty?).to be true + expect(result.slug).to eq 'child-campaign_000' end end @@ -63,9 +64,9 @@ describe CreatePeerToPeerCampaign do Timecop.freeze(2020, 4, 5) do result = CreatePeerToPeerCampaign.create(campaign_params, profile.id) - expect(result).to be_kind_of Hash - expect(result).to include 'errors' - expect(result['errors']['goal_amount']).to match ["can't be blank", 'is not a number'] + expect(result).to be_kind_of Campaign + expect(result.errors.empty?).to be false + expect(result.errors['goal_amount']).to match ["can't be blank", 'is not a number'] end end From d81c3e5ec8936bebc57ce79b6a30b1ce04890c3a Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 21 May 2020 17:22:22 -0500 Subject: [PATCH 410/440] Move campaigns/index.json to jbuilder --- app/views/campaigns/index.jbuilder | 7 +++++++ app/views/campaigns/index.rabl | 9 --------- config/routes.rb | 8 +++++++- spec/controllers/campaigns_spec.rb | 2 +- 4 files changed, 15 insertions(+), 11 deletions(-) create mode 100644 app/views/campaigns/index.jbuilder delete mode 100644 app/views/campaigns/index.rabl diff --git a/app/views/campaigns/index.jbuilder b/app/views/campaigns/index.jbuilder new file mode 100644 index 00000000..799fa599 --- /dev/null +++ b/app/views/campaigns/index.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @campaigns do |campaign| + json.extract! campaign, :name, :total_raised, :goal_amount, :id + json.url campaign_locateable_url(campaign) +end \ No newline at end of file diff --git a/app/views/campaigns/index.rabl b/app/views/campaigns/index.rabl deleted file mode 100644 index d180c586..00000000 --- a/app/views/campaigns/index.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @campaigns => :data do - collection @campaigns, object_root: false - attributes :name, :total_raised, :goal_amount, :url, :id -end diff --git a/config/routes.rb b/config/routes.rb index 6e1f8e6d..c8c09e9e 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -230,7 +230,7 @@ Rails.application.routes.draw do # Campaigns match 'campaigns' => 'campaigns#index', via: %i[get post] - match 'campaigns/:campaign_slug' => 'campaigns#show', via: %i[get post] + match 'campaigns/:campaign_slug' => 'campaigns#show', via: %i[get post], as: :campaign_location match 'campaigns/:campaign_slug/supporters' => 'campaigns/supporters#index', via: %i[get post] @@ -245,6 +245,12 @@ Rails.application.routes.draw do end end + direct :campaign_locateable do |model| + nonprofit = model.nonprofit + {controller: 'campaigns', action: "show", state_code: nonprofit.state_code_slug, city: nonprofit.city_slug, name: nonprofit.slug, + campaign_slug: model.slug} + end + # Misc get '/pages/wp-plugin', to: redirect('/help/wordpress-plugin') # temporary, until WP plugin updated diff --git a/spec/controllers/campaigns_spec.rb b/spec/controllers/campaigns_spec.rb index 14ff9127..fb0c0fb0 100644 --- a/spec/controllers/campaigns_spec.rb +++ b/spec/controllers/campaigns_spec.rb @@ -85,7 +85,7 @@ describe CampaignsController, type: :controller do name: 'simplename', total_raised: 0, goal_amount: 444, - url: "/nm/albuquerque/new-mexico-equality/campaigns/slug_#{campaign.id}" + url: "http://test.host/nm/albuquerque/new-mexico-equality/campaigns/slug_#{campaign.id}" }]}.with_indifferent_access) end end From 085be0c08203281375c56b1b158cfbe58dd5195d Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 22 May 2020 16:00:16 -0500 Subject: [PATCH 411/440] Migrate recurring_donations#show.json to jbuilder --- .../recurring_donations/show.json.jbuilder | 21 +++++++++++++++++++ .../nonprofits/recurring_donations/show.rabl | 17 --------------- 2 files changed, 21 insertions(+), 17 deletions(-) create mode 100644 app/views/nonprofits/recurring_donations/show.json.jbuilder delete mode 100644 app/views/nonprofits/recurring_donations/show.rabl diff --git a/app/views/nonprofits/recurring_donations/show.json.jbuilder b/app/views/nonprofits/recurring_donations/show.json.jbuilder new file mode 100644 index 00000000..d9e984cb --- /dev/null +++ b/app/views/nonprofits/recurring_donations/show.json.jbuilder @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data recurring_donation do |rd| + json.extract! rd, :id, :total_given, + :supporter_id, :interval, + :time_unit, :designation, + :anonymous, :start_date, :end_date, + :created_at, :paydate, :edit_token + json.donation recurring_donation.donation do |d| + json.extract! d, :id, :amount, :designation + end + + json.supporter recurring_donation.supporter do |s| + json.extract! s, :name, :email, :id, :anonymous + end + + json.card recurring_donation.card do |c| + json.extract! c, :name + end +end \ No newline at end of file diff --git a/app/views/nonprofits/recurring_donations/show.rabl b/app/views/nonprofits/recurring_donations/show.rabl deleted file mode 100644 index f8040f1a..00000000 --- a/app/views/nonprofits/recurring_donations/show.rabl +++ /dev/null @@ -1,17 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object @recurring_donation => :data -attributes :id, :total_given, :supporter_id, :interval, :time_unit, :designation, :anonymous, :start_date, :end_date, :created_at, :paydate, :edit_token - -child :donation do - attributes :amount, :designation -end - -child :supporter do - attributes :name, :email, :id, :anonymous -end - -child :card do - attributes :name -end From 1eb6a949360b8abe6aee849b0299dd5977e3ec0a Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 26 May 2020 14:08:57 -0500 Subject: [PATCH 412/440] Add event_locateable to routes --- config/routes.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/routes.rb b/config/routes.rb index c8c09e9e..c5cbf7dd 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -251,6 +251,12 @@ Rails.application.routes.draw do campaign_slug: model.slug} end + direct :event_locateable do |model, **opts| + nonprofit = model.nonprofit + {controller: '/events', action: "show", state_code: nonprofit.state_code_slug, city: nonprofit.city_slug, name: nonprofit.slug, + event_slug: model.slug}.merge(**opts) + end + # Misc get '/pages/wp-plugin', to: redirect('/help/wordpress-plugin') # temporary, until WP plugin updated From 8c524e6c57939186e4f3c678af6e086329bd3639 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 10:10:52 -0500 Subject: [PATCH 413/440] Add way to pass additional options to the campaign_locateable route helper --- config/routes.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/routes.rb b/config/routes.rb index c5cbf7dd..eb21a472 100755 --- a/config/routes.rb +++ b/config/routes.rb @@ -245,10 +245,10 @@ Rails.application.routes.draw do end end - direct :campaign_locateable do |model| + direct :campaign_locateable do |model, **opts| nonprofit = model.nonprofit {controller: 'campaigns', action: "show", state_code: nonprofit.state_code_slug, city: nonprofit.city_slug, name: nonprofit.slug, - campaign_slug: model.slug} + campaign_slug: model.slug}.merge(**opts) end direct :event_locateable do |model, **opts| From 94b2e8e2825b61d8458477bb6026a7a1dc69e9a4 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 10:24:10 -0500 Subject: [PATCH 414/440] Specs can now pass in an expected status code --- spec/controllers/support/shared_user_context.rb | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/spec/controllers/support/shared_user_context.rb b/spec/controllers/support/shared_user_context.rb index fe9f29e6..e5c02ad9 100644 --- a/spec/controllers/support/shared_user_context.rb +++ b/spec/controllers/support/shared_user_context.rb @@ -85,12 +85,12 @@ RSpec.shared_context :shared_user_context do end def accept(user_to_signin, method, action, *args) - without_json_response = method_without_json_view?(args) - request.accept = 'application/json' unless without_json_response + test_variables = collect_test_variables(args) + request.accept = 'application/json' unless test_variables[:without_json_view] sign_in user_to_signin if user_to_signin # allows us to run the helpers but ignore what the controller action does - if without_json_response + if test_variables[:without_json_view] expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(200)) expect_any_instance_of(described_class).to receive(:render).and_return(nil) send(method, action, reduce_params(*args)) @@ -98,7 +98,7 @@ RSpec.shared_context :shared_user_context do else expect_any_instance_of(described_class).to receive(action).and_return(ActionDispatch::IntegrationTest.new(204)) send(method, action, reduce_params(*args)) - expect(response.status).to eq 204 + expect(response.status).to eq(test_variables[:with_status] || 204) end end @@ -114,15 +114,16 @@ RSpec.shared_context :shared_user_context do { params: args.reduce({}, :merge) } end - def method_without_json_view?(*args) + def collect_test_variables(*args) + test_vars = {} args.collect do |items| if items.kind_of?(Array) items.each do |k, v| - return k[:without_json_view] if k.kind_of?(Hash) && k[:without_json_view] + test_vars.merge!(k.slice(:without_json_view, :with_status)) if k.kind_of?(Hash) end end end - return false + return test_vars end def fix_args(*args) From 4b01c2aeb938e7249f1ff6770a1909de4cac9c68 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 10:25:52 -0500 Subject: [PATCH 415/440] Update nonprofits/recurring_donations_controller to Rails v6 and jbuilder --- .../nonprofits/recurring_donations_controller.rb | 6 +++++- .../{show.json.jbuilder => show.jbuilder} | 0 spec/controllers/nonprofits/recurring_donations_spec.rb | 2 +- 3 files changed, 6 insertions(+), 2 deletions(-) rename app/views/nonprofits/recurring_donations/{show.json.jbuilder => show.jbuilder} (100%) diff --git a/app/controllers/nonprofits/recurring_donations_controller.rb b/app/controllers/nonprofits/recurring_donations_controller.rb index 2623484c..78fed2f4 100644 --- a/app/controllers/nonprofits/recurring_donations_controller.rb +++ b/app/controllers/nonprofits/recurring_donations_controller.rb @@ -56,7 +56,11 @@ module Nonprofits def show @recurring_donation = current_recurring_donation - respond_to { |format| format.json } + respond_to do |format| + format.json do + render locals: {recurring_donation: @recurring_donation} + end + end end def destroy diff --git a/app/views/nonprofits/recurring_donations/show.json.jbuilder b/app/views/nonprofits/recurring_donations/show.jbuilder similarity index 100% rename from app/views/nonprofits/recurring_donations/show.json.jbuilder rename to app/views/nonprofits/recurring_donations/show.jbuilder diff --git a/spec/controllers/nonprofits/recurring_donations_spec.rb b/spec/controllers/nonprofits/recurring_donations_spec.rb index f9300cd3..4a23e1c0 100644 --- a/spec/controllers/nonprofits/recurring_donations_spec.rb +++ b/spec/controllers/nonprofits/recurring_donations_spec.rb @@ -16,7 +16,7 @@ describe Nonprofits::RecurringDonationsController, type: :controller do end describe 'show' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1', without_json_view: true + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1', with_status: 200 end describe 'destroy' do From 2854603d56fbefadecc99dcba8d922780c0c1555 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 10:28:08 -0500 Subject: [PATCH 416/440] Update nonprofits/payments_controller to Rails 6 and jbuilder --- .../nonprofits/payments_controller.rb | 1 + app/views/nonprofits/payments/show.jbuilder | 78 +++++++++++++++++++ app/views/nonprofits/payments/show.rabl | 72 ----------------- spec/controllers/nonprofits/payments_spec.rb | 2 +- 4 files changed, 80 insertions(+), 73 deletions(-) create mode 100644 app/views/nonprofits/payments/show.jbuilder delete mode 100644 app/views/nonprofits/payments/show.rabl diff --git a/app/controllers/nonprofits/payments_controller.rb b/app/controllers/nonprofits/payments_controller.rb index 8bea6257..63a03ab7 100644 --- a/app/controllers/nonprofits/payments_controller.rb +++ b/app/controllers/nonprofits/payments_controller.rb @@ -42,6 +42,7 @@ module Nonprofits def show @nonprofit = current_nonprofit @payment = @nonprofit.payments.find(params[:id]) + render locals: {payment: @payment} end # def show def update diff --git a/app/views/nonprofits/payments/show.jbuilder b/app/views/nonprofits/payments/show.jbuilder new file mode 100644 index 00000000..f2f63a89 --- /dev/null +++ b/app/views/nonprofits/payments/show.jbuilder @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data do + json.extract! @payment, :id, :gross_amount, + :towards, :net_amount, + :fee_total, :date, + :refund_total, :kind + + d_anonymous = @payment.donation.nil? ? false : @payment.donation.anonymous + json.consider_donation_anonymous (!!d_anonymous || !!@payment.supporter.anonymous) + + json.charge do + json.extract! @payment.charge, :created_at, :id, :status + end + + json.donation do + d = @payment.donation + json.extract! d, :designation, :dedication, :origin_url, :id, :comment + + + json.campaign do + c = d.campaign + json.extract! c, :name, :id + json.url campaign_locateable_url(c) + end if d.campaign + + + json.campaign_gift do + json.name(d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil) + end + + json.event(d.event, partial: "events/event", as: :event) if d.event + + json.recurring_donation do + rd = d.recurring_donation + json.extract! rd, :interval, :time_unit, :created_at + end if d.recurring_donation + end if @payment.donation + + json.dispute do + dis =@payment.dispute + json.extract! dis, :id, :status, :reason + end if @payment.dispute + + json.refund do + ref = @payment.refund + json.extract! ref, :reason, :comment, :disbursed + end if @payment.refund + + json.offsite_payment do + off_p = @payment.offsite_payment + json.extract! off_p, :check_number, :kind + end if @payment.offsite_payment + + json.ticket do + event = @payment.tickets.last.event + json.event do + json.extract! event, :name, :id + json.url event_locateable_url(event) + end + json.levels @payment.tickets.map { |t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)" }.join(', ') + json.discount @payment.tickets.map { |t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil }.compact.join(', ') + end if @payment&.tickets&.last&.event + + json.tickets @payment.tickets do |t| + json.id t.id + json.ticket_level t.ticket_level, :name + end if @payment.tickets.any? + + json.supporter do + json.extract! @payment.supporter, :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country + end + + json.nonprofit do + json.id @payment.nonprofit.id + end +end \ No newline at end of file diff --git a/app/views/nonprofits/payments/show.rabl b/app/views/nonprofits/payments/show.rabl deleted file mode 100644 index c63e7e72..00000000 --- a/app/views/nonprofits/payments/show.rabl +++ /dev/null @@ -1,72 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object @payment => :data - -attributes :gross_amount, :towards, :net_amount, :fee_total, :id, :date, :refund_total, :kind - -node(:consider_donation_anonymous) do |p| - d_anonymous = p.donation.nil? ? false : p.donation.anonymous - - !!d_anonymous || !!p.supporter.anonymous -end - -child :charge do - attributes :created_at, :id, :status -end - -child :donation, object_root: false do - attributes :designation, :dedication, :origin_url, :id, :comment - - child :campaign, object_root: false do - attributes :name, :url, :id - end - - node(:campaign_gift) { |d| { name: d.campaign_gifts.any? ? d.campaign_gifts.last.campaign_gift_option.name : nil } } - - child :event, object_root: false do - attributes :name, :url, :id - end - - child :recurring_donation, object_root: false do - attributes :interval, :time_unit, :created_at - end -end - -child :dispute, object_root: false do - attributes :id, :status, :reason -end - -child :refund do - attributes :reason, :comment, :disbursed -end - -child :offsite_payment do - attributes :check_number, :kind -end - -node(:ticket) do |payment| - event = payment&.tickets&.last&.event - h = { - event: { name: event&.name, url: event&.url, id: event&.id }, - levels: payment.tickets.map { |t| "#{GetData.chain(t.ticket_level, :name)} (#{t.quantity}x)" }.join(', '), - discount: payment.tickets.map { |t| t.event_discount ? "#{t.event_discount.name} (#{t.event_discount.percent}%)" : nil }.compact.join(', ') - } - event ? h : nil -end - -child :tickets, object_root: false do - attributes :id - - child :ticket_level do - attributes :name - end -end - -child :supporter do - attributes :name, :email, :city, :state_code, :address, :zip_code, :phone, :id, :country -end - -child :nonprofit do - attributes :id -end diff --git a/spec/controllers/nonprofits/payments_spec.rb b/spec/controllers/nonprofits/payments_spec.rb index dad9a4f4..ed3696f2 100644 --- a/spec/controllers/nonprofits/payments_spec.rb +++ b/spec/controllers/nonprofits/payments_spec.rb @@ -16,7 +16,7 @@ describe Nonprofits::PaymentsController, type: :controller do end describe 'show payments' do - include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1', without_json_view: true + include_context :open_to_np_associate, :get, :show, nonprofit_id: :__our_np, id: '1', with_status: 200 end describe 'update' do From 5ff8418f73c3676257829e44ba2146e5006cb13c Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 10:41:22 -0500 Subject: [PATCH 417/440] Update custom_field_joins/index to Rails 6 and jbuilder --- app/views/nonprofits/custom_field_joins/index.jbuilder | 8 ++++++++ app/views/nonprofits/custom_field_joins/index.rabl | 9 --------- 2 files changed, 8 insertions(+), 9 deletions(-) create mode 100644 app/views/nonprofits/custom_field_joins/index.jbuilder delete mode 100644 app/views/nonprofits/custom_field_joins/index.rabl diff --git a/app/views/nonprofits/custom_field_joins/index.jbuilder b/app/views/nonprofits/custom_field_joins/index.jbuilder new file mode 100644 index 00000000..7338c221 --- /dev/null +++ b/app/views/nonprofits/custom_field_joins/index.jbuilder @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +json.data @custom_field_joins do |cfj| + json.extract! cfj, :name, :created_at, :id, :value +end + diff --git a/app/views/nonprofits/custom_field_joins/index.rabl b/app/views/nonprofits/custom_field_joins/index.rabl deleted file mode 100644 index 92978134..00000000 --- a/app/views/nonprofits/custom_field_joins/index.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @custom_field_joins => :data do - collection @custom_field_joins, object_root: false - attributes :name, :created_at, :id, :value -end From 9070aee6b8117459fe3537f8e441d6f03b14d6f0 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 12:42:21 -0500 Subject: [PATCH 418/440] Fix typo in name of test --- spec/controllers/nonprofits/custom_field_masters_spec.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/controllers/nonprofits/custom_field_masters_spec.rb b/spec/controllers/nonprofits/custom_field_masters_spec.rb index a6bb2afc..585d6685 100644 --- a/spec/controllers/nonprofits/custom_field_masters_spec.rb +++ b/spec/controllers/nonprofits/custom_field_masters_spec.rb @@ -7,7 +7,7 @@ require 'controllers/support/shared_user_context' describe Nonprofits::CustomFieldMastersController, type: :controller do include_context :shared_user_context describe 'rejects unauthenticated users' do - describe 'get payments' do + describe 'get custom field masters' do include_context :open_to_np_associate, :get, :index, nonprofit_id: :__our_np, without_json_view: true end From b02aa85e58e218d42e563aa6397fbdf6dfe7a0b8 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 12:49:04 -0500 Subject: [PATCH 419/440] Update nonprofits/custom_field_masters_controller.rb to Rails 6 and jbuilder --- app/views/nonprofits/custom_field_masters/index.jbuilder | 6 ++++++ app/views/nonprofits/custom_field_masters/index.rabl | 9 --------- 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 app/views/nonprofits/custom_field_masters/index.jbuilder delete mode 100644 app/views/nonprofits/custom_field_masters/index.rabl diff --git a/app/views/nonprofits/custom_field_masters/index.jbuilder b/app/views/nonprofits/custom_field_masters/index.jbuilder new file mode 100644 index 00000000..5f36e88e --- /dev/null +++ b/app/views/nonprofits/custom_field_masters/index.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @custom_field_masters do |cfm| + json.extract! cfm, :name, :id, :created_at +end diff --git a/app/views/nonprofits/custom_field_masters/index.rabl b/app/views/nonprofits/custom_field_masters/index.rabl deleted file mode 100644 index b211fd1e..00000000 --- a/app/views/nonprofits/custom_field_masters/index.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @custom_field_masters => :data do - collection @custom_field_masters, object_root: false - attributes :name, :id, :created_at -end From dbd993fd8e5450173832e95c4dc0ebad652ab279 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 12:59:13 -0500 Subject: [PATCH 420/440] Convert MapsController to Rails 6 and jbuilder --- app/views/maps/all_npo_supporters.jbuilder | 6 ++++++ app/views/maps/all_npo_supporters.rabl | 9 --------- app/views/maps/all_npos.jbuilder | 6 ++++++ app/views/maps/all_npos.rabl | 9 --------- app/views/maps/all_supporters.jbuilder | 6 ++++++ app/views/maps/all_supporters.rabl | 9 --------- app/views/maps/specific_npo_supporters.jbuilder | 7 +++++++ app/views/maps/specific_npo_supporters.rabl | 9 --------- 8 files changed, 25 insertions(+), 36 deletions(-) create mode 100644 app/views/maps/all_npo_supporters.jbuilder delete mode 100644 app/views/maps/all_npo_supporters.rabl create mode 100644 app/views/maps/all_npos.jbuilder delete mode 100644 app/views/maps/all_npos.rabl create mode 100644 app/views/maps/all_supporters.jbuilder delete mode 100644 app/views/maps/all_supporters.rabl create mode 100644 app/views/maps/specific_npo_supporters.jbuilder delete mode 100644 app/views/maps/specific_npo_supporters.rabl diff --git a/app/views/maps/all_npo_supporters.jbuilder b/app/views/maps/all_npo_supporters.jbuilder new file mode 100644 index 00000000..ead0e879 --- /dev/null +++ b/app/views/maps/all_npo_supporters.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @map_data do |md| + json.extract! md, :name, :latitude, :longitude, :id +end diff --git a/app/views/maps/all_npo_supporters.rabl b/app/views/maps/all_npo_supporters.rabl deleted file mode 100644 index 9dd4bbfc..00000000 --- a/app/views/maps/all_npo_supporters.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id -end diff --git a/app/views/maps/all_npos.jbuilder b/app/views/maps/all_npos.jbuilder new file mode 100644 index 00000000..084c850f --- /dev/null +++ b/app/views/maps/all_npos.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @map_data do |md| + json.extract! md, :name, :latitude, :longitude, :id, :email, :phone, :website +end diff --git a/app/views/maps/all_npos.rabl b/app/views/maps/all_npos.rabl deleted file mode 100644 index 2b04899e..00000000 --- a/app/views/maps/all_npos.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone, :website -end diff --git a/app/views/maps/all_supporters.jbuilder b/app/views/maps/all_supporters.jbuilder new file mode 100644 index 00000000..512dc9fe --- /dev/null +++ b/app/views/maps/all_supporters.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @map_data do |md| + json.extract! md, :name, :latitude, :longitude, :id, :email, :phone +end diff --git a/app/views/maps/all_supporters.rabl b/app/views/maps/all_supporters.rabl deleted file mode 100644 index 0d64c6b1..00000000 --- a/app/views/maps/all_supporters.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone -end diff --git a/app/views/maps/specific_npo_supporters.jbuilder b/app/views/maps/specific_npo_supporters.jbuilder new file mode 100644 index 00000000..2cfe8a0e --- /dev/null +++ b/app/views/maps/specific_npo_supporters.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.data @map_data do |md| + json.extract! md, :name, :latitude, :longitude, :id, :email, :phone, + :address, :city, :state_code, :total_raised +end diff --git a/app/views/maps/specific_npo_supporters.rabl b/app/views/maps/specific_npo_supporters.rabl deleted file mode 100644 index 2d0ff097..00000000 --- a/app/views/maps/specific_npo_supporters.rabl +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -object false - -child @map_data => :data do - collection @map_data, object_root: false - attributes :name, :latitude, :longitude, :id, :email, :phone, :address, :city, :state_code, :total_raised -end From a41176eb0f500af89e2af1f319ec9d5abf834bfa Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 13:07:17 -0500 Subject: [PATCH 421/440] Update to webpacker 5.1 --- Gemfile | 2 +- Gemfile.lock | 4 +- babel.config.js | 3 +- config/webpack/environment.js | 3 - config/webpack/loaders/typescript.js | 11 -- package.json | 6 +- yarn.lock | 258 +++++++++++++++------------ 7 files changed, 148 insertions(+), 139 deletions(-) delete mode 100644 config/webpack/loaders/typescript.js diff --git a/Gemfile b/Gemfile index 1e897b6f..a755166c 100755 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem 'sassc', '~> 2.0', '>= 2.0.1' gem 'stripe', '~> 1.58' # January 19, 2017 version of the Stripe API https://stripe.com/docs/api gem 'uglifier', '~> 4.1', '>= 4.1.20' gem 'ffi', '~> 1.11', '>= 1.11.1' -gem 'webpacker', '~> 5.0.1' +gem 'webpacker', '~> 5.1' gem 'httparty', '~> 0.17.0' # https://github.com/jnunemaker/httparty gem 'rack-attack', '~> 5.2' # for blocking ip addressses diff --git a/Gemfile.lock b/Gemfile.lock index ec5f9b76..b3f6e9b5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -433,7 +433,7 @@ GEM addressable (>= 2.3.6) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webpacker (5.0.1) + webpacker (5.1.1) activesupport (>= 5.2) rack-proxy (>= 0.6.1) railties (>= 5.2) @@ -522,7 +522,7 @@ DEPENDENCIES traceroute (~> 0.8.0) uglifier (~> 4.1, >= 4.1.20) webmock (~> 3.6, >= 3.6.2) - webpacker (~> 5.0.1) + webpacker (~> 5.1) wisper (~> 2.0) wisper-activejob (~> 1.0.0) wisper-rspec (~> 1.1.0) diff --git a/babel.config.js b/babel.config.js index 84888b61..79a42749 100644 --- a/babel.config.js +++ b/babel.config.js @@ -34,7 +34,8 @@ module.exports = function(api) { modules: false, exclude: ['transform-typeof-symbol'] } - ] + ], + ['@babel/preset-typescript', { 'allExtensions': true, 'isTSX': true }] ].filter(Boolean), plugins: [ require('babel-plugin-macros'), diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 7c2f767a..7ee10f4d 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -1,8 +1,5 @@ const { environment } = require('@rails/webpacker') const erb = require('./loaders/erb') -const typescript = require('./loaders/typescript') - -environment.loaders.prepend('typescript', typescript) environment.loaders.prepend('erb', erb) diff --git a/config/webpack/loaders/typescript.js b/config/webpack/loaders/typescript.js deleted file mode 100644 index a3cb0ef9..00000000 --- a/config/webpack/loaders/typescript.js +++ /dev/null @@ -1,11 +0,0 @@ -const PnpWebpackPlugin = require('pnp-webpack-plugin') - -module.exports = { - test: /\.tsx?(\.erb)?$/, - use: [ - { - loader: 'ts-loader', - options: PnpWebpackPlugin.tsLoaderOptions() - } - ] -} diff --git a/package.json b/package.json index 19e8dd2c..7a0abbd1 100644 --- a/package.json +++ b/package.json @@ -50,17 +50,17 @@ "sinon": "^5.0.7", "style-loader": "^0.21.0", "ts-jest": "^24.0.0", - "ts-loader": "^6.2.1", "typescript": "^3.7.2", "url-loader": "^1.0.1", "webpack": "^4.0.0", "webpack-cli": "^3.3.11", - "webpack-dev-server": "^3.9.0" + "webpack-dev-server": "^3.11.0" }, "dependencies": { "@babel/core": "^7.0.0", + "@babel/preset-typescript": "^7.9.0", "@rails/activestorage": "^6.0.2-2", - "@rails/webpacker": "~5.0.1", + "@rails/webpacker": "~5.1.1", "attr-binder": "0.3.1", "aws-sdk": "^2.402.0", "chart.js": "2.1.4", diff --git a/yarn.lock b/yarn.lock index bebb673e..8c8d953c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,6 +40,16 @@ semver "^5.4.1" source-map "^0.5.0" +"@babel/generator@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.0.tgz#a238837896edf35ee5fbfb074548d3256b4bc55d" + integrity sha512-ThoWCJHlgukbtCP79nAK4oLqZt5fVo70AHUni/y8Jotyg5rtJiG2FVl+iJjRNKIyl4hppqztLyAoEWcCvqyOFQ== + dependencies: + "@babel/types" "^7.10.0" + jsesc "^2.5.1" + lodash "^4.17.13" + source-map "^0.5.0" + "@babel/generator@^7.4.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" @@ -76,6 +86,18 @@ levenary "^1.1.1" semver "^5.5.0" +"@babel/helper-create-class-features-plugin@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.0.tgz#3a2b7b86f6365ea4ac3837a49ec5791e65217944" + integrity sha512-n4tPJaI0iuLRayriXTQ8brP3fMA/fNmxpxswfNuhe4qXQbcCWzeAqm6SeR/KExIOcdCvOh/KkPQVgBsjcb0oqA== + dependencies: + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-member-expression-to-functions" "^7.10.0" + "@babel/helper-optimise-call-expression" "^7.10.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/helper-replace-supers" "^7.10.0" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/helper-create-class-features-plugin@^7.8.3": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.5.tgz#79753d44017806b481017f24b02fd4113c7106ea" @@ -137,6 +159,13 @@ dependencies: "@babel/types" "^7.8.3" +"@babel/helper-member-expression-to-functions@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.10.0.tgz#e8cf57470bfd1247f2b41aa621a527e952efa6f1" + integrity sha512-xKLTpbMkJcvwEsDaTfs9h0IlfUiBLPFfybxaPpPPsQDsZTRg+UKh+86oK7sctHF3OUiRQkb10oS9MXSqgyV6/g== + dependencies: + "@babel/types" "^7.10.0" + "@babel/helper-member-expression-to-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" @@ -164,6 +193,13 @@ "@babel/types" "^7.9.0" lodash "^4.17.13" +"@babel/helper-optimise-call-expression@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.0.tgz#6dcfb565842f43bed31b24f3e4277f18826e5e79" + integrity sha512-HgMd8QKA8wMJs5uK/DYKdyzJAEuGt1zyDp9wLMlMR6LitTQTHPUE+msC82ZsEDwq+U3/yHcIXIngRm9MS4IcIg== + dependencies: + "@babel/types" "^7.10.0" + "@babel/helper-optimise-call-expression@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" @@ -194,6 +230,16 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" +"@babel/helper-replace-supers@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.0.tgz#26bc22ee1a35450934d2e2a9b27de10a22fac9d6" + integrity sha512-erl4iVeiANf14JszXP7b69bSrz3e3+qW09pVvEmTWwzRQEOoyb1WFlYCA8d/VjVZGYW8+nGpLh7swf9CifH5wg== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.10.0" + "@babel/helper-optimise-call-expression" "^7.10.0" + "@babel/traverse" "^7.10.0" + "@babel/types" "^7.10.0" + "@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": version "7.8.6" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" @@ -257,6 +303,11 @@ resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== +"@babel/parser@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.0.tgz#8eca3e71a73dd562c5222376b08253436bb4995b" + integrity sha512-fnDUl1Uy2gThM4IFVW4ISNHqr3cJrCsRkSCasFgx0XDO9JcttDS5ytyBc4Cu4X1+fjoo3IVvFbRD6TeFlHJlEQ== + "@babel/plugin-proposal-async-generator-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.8.3.tgz#bad329c670b382589721b27540c7d288601c6e6f" @@ -402,6 +453,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-syntax-typescript@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.8.3.tgz#c1f659dda97711a569cef75275f7e15dcaa6cabc" + integrity sha512-GO1MQ/SGGGoiEXY0e0bSpHimJvxqB7lktLLIq2pv8xG7WZ8IMEle74jIe1FhprHBWjwjZtXHkycDLZXIWM5Wfg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-arrow-functions@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.8.3.tgz#82776c2ed0cd9e1a49956daeb896024c9473b8b6" @@ -648,6 +706,15 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-typescript@^7.9.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.10.0.tgz#00273cddb1f5321af09db5c096bb865eab137124" + integrity sha512-BGH4yn+QwYFfzh8ITmChwrcvhLf+jaYBlz+T87CNKTP49SbqrjqTsMqtFijivYWYjcYHvac8II53RYd82vRaAw== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-typescript" "^7.8.3" + "@babel/plugin-transform-unicode-regex@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" @@ -733,6 +800,14 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/preset-typescript@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.9.0.tgz#87705a72b1f0d59df21c179f7c3d2ef4b16ce192" + integrity sha512-S4cueFnGrIbvYJgwsVFKdvOmpiL0XGw9MFW9D0vgRys5g36PBhZRL8NX8Gr2akz8XRtzq6HuDXPD/1nniagNUg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-typescript" "^7.9.0" + "@babel/runtime@^7.1.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": version "7.9.2" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" @@ -764,6 +839,21 @@ globals "^11.1.0" lodash "^4.17.13" +"@babel/traverse@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.0.tgz#290935529881baf619398d94fd453838bef36740" + integrity sha512-NZsFleMaLF1zX3NxbtXI/JCs2RPOdpGru6UBdGsfhdsDsP+kFF+h2QQJnMJglxk0kc69YmMFs4A44OJY0tKo5g== + dependencies: + "@babel/code-frame" "^7.8.3" + "@babel/generator" "^7.10.0" + "@babel/helper-function-name" "^7.9.5" + "@babel/helper-split-export-declaration" "^7.8.3" + "@babel/parser" "^7.10.0" + "@babel/types" "^7.10.0" + debug "^4.1.0" + globals "^11.1.0" + lodash "^4.17.13" + "@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5": version "7.9.5" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" @@ -773,6 +863,15 @@ lodash "^4.17.13" to-fast-properties "^2.0.0" +"@babel/types@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.0.tgz#d47d92249e42393a5723aad5319035ae411e3e38" + integrity sha512-t41W8yWFyQFPOAAvPvjyRhejcLGnJTA3iRpFcDbEKwVJ3UnHQePFzLk8GagTsucJlImyNwrGikGsYURrWbQG8w== + dependencies: + "@babel/helper-validator-identifier" "^7.9.5" + lodash "^4.17.13" + to-fast-properties "^2.0.0" + "@cnakazawa/watch@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@cnakazawa/watch/-/watch-1.0.4.tgz#f864ae85004d0fcab6f50be9141c4da368d1656a" @@ -941,10 +1040,10 @@ dependencies: spark-md5 "^3.0.0" -"@rails/webpacker@~5.0.1": - version "5.0.1" - resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.0.1.tgz#f7bdb8d0b36e41aa2f219aad332f50194ad4fc46" - integrity sha512-r74Od+YO5OxkrePNLL9M3Mi3DQNPLSXkv+cGDdiFvTGeFc+VtI8j0sNGoulBMlEZFH8GVlb7LLy/FVLjPJq1/Q== +"@rails/webpacker@~5.1.1": + version "5.1.1" + resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.1.1.tgz#3c937aa719e46341f037a3f37349ef58085950df" + integrity sha512-ho5Stv9naZgG4HbHNFPqbA1OLPJyj6QXfgAc7VGCu4kkMe/RnVFLoLJFW6TZ9wYelKodBjRA2tKKiCaugv0sZw== dependencies: "@babel/core" "^7.9.0" "@babel/plugin-proposal-class-properties" "^7.8.3" @@ -2081,13 +2180,6 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: - version "3.0.2" - resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" - integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== - dependencies: - fill-range "^7.0.1" - brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2432,7 +2524,7 @@ chalk@2.4.1: escape-string-regexp "^1.0.5" supports-color "^5.3.0" -chalk@2.4.2, chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -2567,15 +2659,6 @@ cliui@^3.2.0: strip-ansi "^3.0.1" wrap-ansi "^2.0.0" -cliui@^4.0.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-4.1.0.tgz#348422dbe82d800b3022eef4f6ac10bf2e4d1b49" - integrity sha512-4FG+RSG9DL7uEwRUZXZn3SS34DiDPfzP0VOiEwtUWlE+AR2EIg+hSyvrIgUUfhdgR/UkAeW2QHgeP+hWrXs7jQ== - dependencies: - string-width "^2.1.1" - strip-ansi "^4.0.0" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3706,7 +3789,7 @@ enhanced-resolve@4.1.0: memory-fs "^0.4.0" tapable "^1.0.0" -enhanced-resolve@^4.0.0, enhanced-resolve@^4.1.0: +enhanced-resolve@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/enhanced-resolve/-/enhanced-resolve-4.1.1.tgz#2937e2b8066cd0fe7ce0990a98f0d71a35189f66" integrity sha512-98p2zE+rL7/g/DzMHMTF4zZlCgeVdJ7yr6xzEpJRYwFYrGi9ANdn5DnJURg6RpBkyk60XYDnWIv51VfIhfNGuA== @@ -4250,13 +4333,6 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" -fill-range@^7.0.1: - version "7.0.1" - resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" - integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== - dependencies: - to-regex-range "^5.0.1" - finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -4983,7 +5059,7 @@ html-encoding-sniffer@^1.0.2: dependencies: whatwg-encoding "^1.0.1" -html-entities@^1.2.1: +html-entities@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== @@ -5497,11 +5573,6 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" -is-number@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" - integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== - is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" @@ -6596,7 +6667,7 @@ lodash@^4.0.0, lodash@^4.15.0, lodash@^4.16.2, lodash@^4.17.11, lodash@^4.17.13, resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -loglevel@^1.6.6: +loglevel@^1.6.8: version "1.6.8" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== @@ -6813,14 +6884,6 @@ micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: snapdragon "^0.8.1" to-regex "^3.0.2" -micromatch@^4.0.0: - version "4.0.2" - resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.2.tgz#4fcb0999bf9fbc2fcbdd212f6d629b9a56c39259" - integrity sha512-y7FpHSbMUMoyPbYUSzO6PaZ6FyRnQOpHuKwbo1G+Knck95XVU4QAiKdGEnj5wwoS7PlOgthX/09u5iFJ+aYf5Q== - dependencies: - braces "^3.0.1" - picomatch "^2.0.5" - miller-rabin@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/miller-rabin/-/miller-rabin-4.0.1.tgz#f080351c865b0dc562a8462966daa53543c78a4d" @@ -7510,7 +7573,7 @@ os-locale@^1.4.0: dependencies: lcid "^1.0.0" -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -7849,11 +7912,6 @@ phone-formatter@0.0.2: resolved "https://registry.yarnpkg.com/phone-formatter/-/phone-formatter-0.0.2.tgz#f3626c7d274860f014f70f43a87566a16b0e7ace" integrity sha1-82JsfSdIYPAU9w9DqHVmoWsOes4= -picomatch@^2.0.5: - version "2.2.2" - resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" - integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== - pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -7935,10 +7993,10 @@ pnp-webpack-plugin@^1.6.4: dependencies: ts-pnp "^1.1.6" -portfinder@^1.0.25: - version "1.0.25" - resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" - integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== +portfinder@^1.0.26: + version "1.0.26" + resolved "https://registry.yarnpkg.com/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" + integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== dependencies: async "^2.6.2" debug "^3.1.1" @@ -10073,13 +10131,14 @@ sockjs-client@1.4.0: json3 "^3.3.2" url-parse "^1.4.3" -sockjs@0.3.19: - version "0.3.19" - resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" - integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.yarnpkg.com/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== dependencies: faye-websocket "^0.10.0" - uuid "^3.0.1" + uuid "^3.4.0" + websocket-driver "0.6.5" sort-keys@^1.0.0: version "1.1.2" @@ -10182,7 +10241,7 @@ spdy-transport@^3.0.0: readable-stream "^3.0.6" wbuf "^1.7.3" -spdy@^4.0.1: +spdy@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== @@ -10329,7 +10388,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2": version "2.1.1" resolved "https://registry.yarnpkg.com/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -10713,13 +10772,6 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" -to-regex-range@^5.0.1: - version "5.0.1" - resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" - integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== - dependencies: - is-number "^7.0.0" - to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -10778,17 +10830,6 @@ ts-jest@^24.0.0: semver "^5.5" yargs-parser "10.x" -ts-loader@^6.2.1: - version "6.2.2" - resolved "https://registry.yarnpkg.com/ts-loader/-/ts-loader-6.2.2.tgz#dffa3879b01a1a1e0a4b85e2b8421dc0dfff1c58" - integrity sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ== - dependencies: - chalk "^2.3.0" - enhanced-resolve "^4.0.0" - loader-utils "^1.0.2" - micromatch "^4.0.0" - semver "^6.0.0" - ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.yarnpkg.com/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -11020,7 +11061,7 @@ uuid@3.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.0.1, uuid@^3.3.2: +uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -11175,10 +11216,10 @@ webpack-dev-middleware@^3.7.2: range-parser "^1.2.1" webpack-log "^2.0.0" -webpack-dev-server@^3.9.0: - version "3.10.3" - resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0" - integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ== +webpack-dev-server@^3.11.0: + version "3.11.0" + resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -11188,31 +11229,31 @@ webpack-dev-server@^3.9.0: debug "^4.1.1" del "^4.1.1" express "^4.17.1" - html-entities "^1.2.1" + html-entities "^1.3.1" http-proxy-middleware "0.19.1" import-local "^2.0.0" internal-ip "^4.3.0" ip "^1.1.5" is-absolute-url "^3.0.3" killable "^1.0.1" - loglevel "^1.6.6" + loglevel "^1.6.8" opn "^5.5.0" p-retry "^3.0.1" - portfinder "^1.0.25" + portfinder "^1.0.26" schema-utils "^1.0.0" selfsigned "^1.10.7" semver "^6.3.0" serve-index "^1.9.1" - sockjs "0.3.19" + sockjs "0.3.20" sockjs-client "1.4.0" - spdy "^4.0.1" + spdy "^4.0.2" strip-ansi "^3.0.1" supports-color "^6.1.0" url "^0.11.0" webpack-dev-middleware "^3.7.2" webpack-log "^2.0.0" ws "^6.2.1" - yargs "12.0.5" + yargs "^13.3.2" webpack-log@^2.0.0: version "2.0.0" @@ -11259,6 +11300,13 @@ webpack@^4.0.0, webpack@^4.42.1: watchpack "^1.6.1" webpack-sources "^1.4.1" +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -11427,7 +11475,7 @@ y18n@^3.2.1: resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== @@ -11461,14 +11509,6 @@ yargs-parser@10.x: dependencies: camelcase "^4.1.0" -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^13.1.0, yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -11484,24 +11524,6 @@ yargs-parser@^5.0.0: dependencies: camelcase "^3.0.0" -yargs@12.0.5: - version "12.0.5" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - yargs@13.2.4: version "13.2.4" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" @@ -11519,7 +11541,7 @@ yargs@13.2.4: y18n "^4.0.0" yargs-parser "^13.1.0" -yargs@^13.3.0: +yargs@^13.3.0, yargs@^13.3.2: version "13.3.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== From afbc20b8dc67af0b098326825bdc637604d382b9 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 17:05:25 -0500 Subject: [PATCH 422/440] Update to Webpacker 5.1.1 --- Gemfile | 2 +- Gemfile.lock | 2 +- .../legacy/components/nonprofit-branding.js | 2 +- .../edit/custom-nonprofit-branding.es6 | 2 +- .../legacy_react}/api/api/NonprofitsApi.ts | 16 +- .../legacy_react}/api/api/UsersApi.ts | 8 +- .../javascript/legacy_react}/api/api/api.ts | 0 .../legacy_react}/api/configuration.ts | 0 .../javascript/legacy_react}/api/index.ts | 0 .../legacy_react}/api/model/Nonprofit.ts | 0 .../legacy_react}/api/model/PostNonprofit.ts | 0 .../api/model/PostNonprofitNonprofit.ts | 0 .../api/model/PostNonprofitUser.ts | 0 .../legacy_react}/api/model/PostUser.ts | 0 .../api/model/ValidationError.ts | 0 .../api/model/ValidationErrors.ts | 0 .../legacy_react}/api/model/models.ts | 0 .../app/create_new_offsite_payment_pane.tsx | 0 .../legacy_react}/app/edit_payment_pane.tsx | 0 .../legacy_react}/app/loading_indicator.ts | 0 .../legacy_react}/app/registration_page.tsx | 0 .../legacy_react}/app/session_login_page.tsx | 0 .../app/create_new_offsite_payment_pane.tsx | 33 + .../javascripts/app/edit_payment_pane.tsx | 27 + .../javascripts/app/registration_page.tsx | 16 + .../components/common/BootstrapWrapper.tsx | 0 .../components/common/DefaultCloseButton.tsx | 4 +- .../common/LabeledFieldComponent.spec.tsx | 0 .../common/LabeledFieldComponent.tsx | 0 .../src/components/common/Modal.spec.tsx | 0 .../src/components/common/Modal.tsx | 2 +- .../common/ProgressableButton.spec.tsx | 0 .../components/common/ProgressableButton.tsx | 0 .../src/components/common/Root.tsx | 2 +- .../common/ScreenReaderOnlyText.spec.tsx | 0 .../common/ScreenReaderOnlyText.tsx | 0 .../src/components/common/Spinner.spec.tsx | 0 .../src/components/common/Spinner.tsx | 0 .../common/StandardFieldComponent.spec.tsx | 0 .../common/StandardFieldComponent.tsx | 0 .../LabeledFieldComponent.spec.tsx.snap | 0 .../common/__snapshots__/Modal.spec.tsx.snap | 0 .../ProgressableButton.spec.tsx.snap | 0 .../ScreenReaderOnlyText.spec.tsx.snap | 0 .../__snapshots__/Spinner.spec.tsx.snap | 0 .../StandardFieldComponent.spec.tsx.snap | 0 .../common/__snapshots__/layout.spec.tsx.snap | 0 .../src/components/common/fields.tsx | 2 +- .../src/components/common/form/ReactForm.tsx | 0 .../common/form/ReactInput.spec.tsx | 0 .../src/components/common/form/ReactInput.tsx | 0 .../common/form/ReactMaskedInput.tsx | 0 .../common/form/ReactSelect.spec.tsx | 0 .../components/common/form/ReactSelect.tsx | 2 +- .../common/form/ReactTextarea.spec.tsx | 0 .../components/common/form/ReactTextarea.tsx | 2 +- .../__snapshots__/ReactInput.spec.tsx.snap | 0 .../__snapshots__/ReactSelect.spec.tsx.snap | 0 .../__snapshots__/ReactTextarea.spec.tsx.snap | 0 .../common/form/react_input_props.ts | 0 .../src/components/common/layout.spec.tsx | 0 .../src/components/common/layout.tsx | 0 .../SelectableTableRow.spec.tsx | 0 .../SelectableTableRow.tsx | 0 .../common/selectable_table_row/connect.tsx | 0 .../src/components/common/svg/CloseButton.tsx | 2 +- .../src/components/common/svg/checkbox.tsx | 0 .../common/test/react_test_helpers.tsx | 0 .../components/common/test/unique_id_mock.ts | 0 .../src/components/common/wizard/RAT/Tab.ts | 0 .../components/common/wizard/RAT/TabList.ts | 0 .../components/common/wizard/RAT/TabPanel.ts | 0 .../common/wizard/RAT/Wrapper.spec.tsx | 0 .../components/common/wizard/RAT/Wrapper.ts | 2 +- .../RAT/__snapshots__/Wrapper.spec.tsx.snap | 0 .../RAT/abstract_tabcomponent_state.spec.tsx | 0 .../wizard/RAT/abstract_tabcomponent_state.ts | 2 +- .../common/wizard/RAT/specialAssign.ts | 0 .../components/common/wizard/Wizard.spec.tsx | 0 .../src/components/common/wizard/Wizard.tsx | 0 .../common/wizard/WizardPanel.spec.tsx | 0 .../components/common/wizard/WizardPanel.tsx | 0 .../common/wizard/WizardTab.spec.tsx | 0 .../components/common/wizard/WizardTab.tsx | 0 .../common/wizard/WizardTabList.tsx | 0 .../wizard/__snapshots__/Wizard.spec.tsx.snap | 0 .../__snapshots__/WizardPanel.spec.tsx.snap | 0 .../wizard/abstract_wizard_state.spec.tsx | 0 .../common/wizard/abstract_wizard_state.ts | 2 +- .../common/wizard/wizard_state.spec.ts | 0 .../components/common/wizard/wizard_state.ts | 2 +- .../CreateOffsitePaymentPane.tsx | 8 +- .../edit_payment_pane/EditPaymentPane.tsx | 4 +- .../NonprofitInfoForm.spec.tsx | 0 .../registration_page/NonprofitInfoForm.tsx | 2 +- .../NonprofitInfoPanel.spec.tsx | 0 .../registration_page/NonprofitInfoPanel.tsx | 0 .../registration_page/RegistrationPage.tsx | 0 .../registration_page/RegistrationWizard.tsx | 2 +- .../registration_page/UserInfoForm.tsx | 0 .../registration_page/UserInfoPanel.spec.tsx | 0 .../registration_page/UserInfoPanel.tsx | 0 .../session_login_page/SessionLoginForm.tsx | 2 +- .../session_login_page/SessionLoginPage.tsx | 0 .../src/lib/api/create_offsite_donation.ts | 4 +- .../legacy_react}/src/lib/api/put_donation.ts | 4 +- .../legacy_react}/src/lib/api/sign_in.ts | 4 +- .../legacy_react}/src/lib/api_manager.spec.ts | 0 .../legacy_react}/src/lib/api_manager.ts | 4 +- .../javascript/legacy_react}/src/lib/apis.ts | 0 .../src/lib/createNumberMask.spec.ts | 0 .../legacy_react}/src/lib/createNumberMask.ts | 0 .../legacy_react}/src/lib/csrf_interceptor.ts | 2 +- .../javascript/legacy_react}/src/lib/date.ts | 2 +- .../legacy_react}/src/lib/dedication.ts | 0 .../src/lib/deprecated_format.ts | 0 .../legacy_react}/src/lib/format.spec.ts | 0 .../legacy_react}/src/lib/format.ts | 0 .../legacy_react}/src/lib/houdini_form.ts | 2 +- .../legacy_react}/src/lib/mobx_utils.ts | 0 .../src/lib/nonprofitBranding.ts | 2 +- .../src/lib/payments/credit_card.spec.ts | 0 .../src/lib/payments/credit_card.ts | 0 .../legacy_react}/src/lib/regex.spec.ts | 0 .../javascript/legacy_react}/src/lib/regex.ts | 0 .../legacy_react}/src/lib/tests/helpers.ts | 0 .../javascript/legacy_react}/src/lib/utils.ts | 0 .../legacy_react}/src/lib/vjf_rules.ts | 2 +- .../packs/create_new_offsite_payment_pane.js | 2 +- app/javascript/packs/edit_payment_pane.js | 2 +- app/javascript/packs/loading_indicator.ts | 2 +- app/javascript/packs/registration_page.js | 2 +- app/javascript/packs/session_login_page.js | 2 +- babel.config.js | 68 +- config/webpack/environment.js | 12 +- config/webpacker.yml | 8 +- package.json | 16 +- tsconfig.json | 4 +- yarn.lock | 1133 +++++++++-------- 139 files changed, 817 insertions(+), 610 deletions(-) rename {javascripts => app/javascript/legacy_react}/api/api/NonprofitsApi.ts (90%) rename {javascripts => app/javascript/legacy_react}/api/api/UsersApi.ts (91%) rename {javascripts => app/javascript/legacy_react}/api/api/api.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/configuration.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/index.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/Nonprofit.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/PostNonprofit.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/PostNonprofitNonprofit.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/PostNonprofitUser.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/PostUser.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/ValidationError.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/ValidationErrors.ts (100%) rename {javascripts => app/javascript/legacy_react}/api/model/models.ts (100%) rename {javascripts => app/javascript/legacy_react}/app/create_new_offsite_payment_pane.tsx (100%) rename {javascripts => app/javascript/legacy_react}/app/edit_payment_pane.tsx (100%) rename {javascripts => app/javascript/legacy_react}/app/loading_indicator.ts (100%) rename {javascripts => app/javascript/legacy_react}/app/registration_page.tsx (100%) rename {javascripts => app/javascript/legacy_react}/app/session_login_page.tsx (100%) create mode 100644 app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx create mode 100644 app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx create mode 100644 app/javascript/legacy_react/javascripts/app/registration_page.tsx rename {javascripts => app/javascript/legacy_react}/src/components/common/BootstrapWrapper.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/DefaultCloseButton.tsx (97%) rename {javascripts => app/javascript/legacy_react}/src/components/common/LabeledFieldComponent.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/LabeledFieldComponent.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/Modal.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/Modal.tsx (97%) rename {javascripts => app/javascript/legacy_react}/src/components/common/ProgressableButton.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/ProgressableButton.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/Root.tsx (95%) rename {javascripts => app/javascript/legacy_react}/src/components/common/ScreenReaderOnlyText.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/ScreenReaderOnlyText.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/Spinner.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/Spinner.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/StandardFieldComponent.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/StandardFieldComponent.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/LabeledFieldComponent.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/Modal.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/ProgressableButton.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/ScreenReaderOnlyText.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/Spinner.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/__snapshots__/layout.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/fields.tsx (98%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactForm.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactInput.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactInput.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactMaskedInput.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactSelect.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactSelect.tsx (96%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactTextarea.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/ReactTextarea.tsx (95%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/__snapshots__/ReactSelect.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/__snapshots__/ReactTextarea.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/form/react_input_props.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/layout.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/layout.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/selectable_table_row/SelectableTableRow.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/selectable_table_row/SelectableTableRow.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/selectable_table_row/connect.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/svg/CloseButton.tsx (96%) rename {javascripts => app/javascript/legacy_react}/src/components/common/svg/checkbox.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/test/react_test_helpers.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/test/unique_id_mock.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/Tab.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/TabList.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/TabPanel.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/Wrapper.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/Wrapper.ts (96%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/__snapshots__/Wrapper.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/abstract_tabcomponent_state.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts (99%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/RAT/specialAssign.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/Wizard.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/Wizard.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/WizardPanel.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/WizardPanel.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/WizardTab.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/WizardTab.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/WizardTabList.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/__snapshots__/Wizard.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/__snapshots__/WizardPanel.spec.tsx.snap (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/abstract_wizard_state.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/abstract_wizard_state.ts (98%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/wizard_state.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/components/common/wizard/wizard_state.ts (99%) rename {javascripts => app/javascript/legacy_react}/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx (98%) rename {javascripts => app/javascript/legacy_react}/src/components/edit_payment_pane/EditPaymentPane.tsx (99%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/NonprofitInfoForm.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/NonprofitInfoForm.tsx (98%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/NonprofitInfoPanel.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/NonprofitInfoPanel.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/RegistrationPage.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/RegistrationWizard.tsx (98%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/UserInfoForm.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/UserInfoPanel.spec.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/registration_page/UserInfoPanel.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/components/session_login_page/SessionLoginForm.tsx (98%) rename {javascripts => app/javascript/legacy_react}/src/components/session_login_page/SessionLoginPage.tsx (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/api/create_offsite_donation.ts (93%) rename {javascripts => app/javascript/legacy_react}/src/lib/api/put_donation.ts (93%) rename {javascripts => app/javascript/legacy_react}/src/lib/api/sign_in.ts (92%) rename {javascripts => app/javascript/legacy_react}/src/lib/api_manager.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/api_manager.ts (94%) rename {javascripts => app/javascript/legacy_react}/src/lib/apis.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/createNumberMask.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/createNumberMask.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/csrf_interceptor.ts (83%) rename {javascripts => app/javascript/legacy_react}/src/lib/date.ts (98%) rename {javascripts => app/javascript/legacy_react}/src/lib/dedication.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/deprecated_format.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/format.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/format.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/houdini_form.ts (99%) rename {javascripts => app/javascript/legacy_react}/src/lib/mobx_utils.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/nonprofitBranding.ts (95%) rename {javascripts => app/javascript/legacy_react}/src/lib/payments/credit_card.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/payments/credit_card.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/regex.spec.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/regex.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/tests/helpers.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/utils.ts (100%) rename {javascripts => app/javascript/legacy_react}/src/lib/vjf_rules.ts (98%) diff --git a/Gemfile b/Gemfile index a755166c..612c0cc6 100755 --- a/Gemfile +++ b/Gemfile @@ -18,7 +18,7 @@ gem 'sassc', '~> 2.0', '>= 2.0.1' gem 'stripe', '~> 1.58' # January 19, 2017 version of the Stripe API https://stripe.com/docs/api gem 'uglifier', '~> 4.1', '>= 4.1.20' gem 'ffi', '~> 1.11', '>= 1.11.1' -gem 'webpacker', '~> 5.1' +gem 'webpacker', '~> 5.1.1' gem 'httparty', '~> 0.17.0' # https://github.com/jnunemaker/httparty gem 'rack-attack', '~> 5.2' # for blocking ip addressses diff --git a/Gemfile.lock b/Gemfile.lock index b3f6e9b5..1a850739 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -522,7 +522,7 @@ DEPENDENCIES traceroute (~> 0.8.0) uglifier (~> 4.1, >= 4.1.20) webmock (~> 3.6, >= 3.6.2) - webpacker (~> 5.1) + webpacker (~> 5.1.1) wisper (~> 2.0) wisper-activejob (~> 1.0.0) wisper-rspec (~> 1.1.0) diff --git a/app/javascript/legacy/components/nonprofit-branding.js b/app/javascript/legacy/components/nonprofit-branding.js index fb9f326f..26501110 100644 --- a/app/javascript/legacy/components/nonprofit-branding.js +++ b/app/javascript/legacy/components/nonprofit-branding.js @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later -import nonprofitBranding from '../../../../javascripts/src/lib/nonprofitBranding'; +import nonprofitBranding from '../../legacy_react/src/lib/nonprofitBranding'; export default nonprofitBranding(app.nonprofit.brand_color) diff --git a/app/javascript/legacy/recurring_donations/edit/custom-nonprofit-branding.es6 b/app/javascript/legacy/recurring_donations/edit/custom-nonprofit-branding.es6 index 3d906cd2..3b2861c3 100644 --- a/app/javascript/legacy/recurring_donations/edit/custom-nonprofit-branding.es6 +++ b/app/javascript/legacy/recurring_donations/edit/custom-nonprofit-branding.es6 @@ -1,4 +1,4 @@ // License: LGPL-3.0-or-later -import nonprofitBranding from '../../../../../javascripts/src/lib/nonprofitBranding'; +import nonprofitBranding from '../../../legacy_react/src/lib/nonprofitBranding'; module.exports = nonprofitBranding diff --git a/javascripts/api/api/NonprofitsApi.ts b/app/javascript/legacy_react/api/api/NonprofitsApi.ts similarity index 90% rename from javascripts/api/api/NonprofitsApi.ts rename to app/javascript/legacy_react/api/api/NonprofitsApi.ts index 0bcce87c..eb145a49 100644 --- a/javascripts/api/api/NonprofitsApi.ts +++ b/app/javascript/legacy_react/api/api/NonprofitsApi.ts @@ -15,7 +15,7 @@ import * as $ from 'jquery'; import * as models from '../model/models'; import { Configuration } from '../configuration'; -const page_info = require('../../../app/javascript/page_info.js.erb') +const page_info = require('../../../page_info.js.erb') /* tslint:disable:no-unused-variable member-ordering */ @@ -89,11 +89,11 @@ export class NonprofitsApi { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); @@ -104,7 +104,7 @@ export class NonprofitsApi { if(false){} else if (xhr.status == 200 && 200 >= 400) { - dfd.reject(new models.NonprofitException(xhr.responseJSON)) + dfd.reject(new models.NonprofitException(xhr.responseJSON as models.Nonprofit)) } else @@ -161,11 +161,11 @@ export class NonprofitsApi { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); @@ -176,12 +176,12 @@ export class NonprofitsApi { if(false){} else if (xhr.status == 201 && 201 >= 400) { - dfd.reject(new models.NonprofitException(xhr.responseJSON)) + dfd.reject(new models.NonprofitException(xhr.responseJSON as models.Nonprofit)) } else if (xhr.status == 400 && 400 >= 400) { - dfd.reject(new models.ValidationErrorsException(xhr.responseJSON)) + dfd.reject(new models.ValidationErrorsException(xhr.responseJSON as models.ValidationErrors)) } else diff --git a/javascripts/api/api/UsersApi.ts b/app/javascript/legacy_react/api/api/UsersApi.ts similarity index 91% rename from javascripts/api/api/UsersApi.ts rename to app/javascript/legacy_react/api/api/UsersApi.ts index cb2e00dc..21f4f669 100644 --- a/javascripts/api/api/UsersApi.ts +++ b/app/javascript/legacy_react/api/api/UsersApi.ts @@ -15,7 +15,7 @@ import * as $ from 'jquery'; import * as models from '../model/models'; import { Configuration } from '../configuration'; -const page_info = require('../../../app/javascript/page_info.js.erb') +const page_info = require('../../../page_info.js.erb') /* tslint:disable:no-unused-variable member-ordering */ @@ -93,11 +93,11 @@ export class UsersApi { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); @@ -107,7 +107,7 @@ export class UsersApi { (xhr: JQueryXHR, textStatus: string, errorThrown: string) => { if (xhr.status == 422) { - dfd.reject(new models.ValidationErrorsException(xhr.responseJSON)) + dfd.reject(new models.ValidationErrorsException(xhr.responseJSON as models.ValidationErrors)) } else diff --git a/javascripts/api/api/api.ts b/app/javascript/legacy_react/api/api/api.ts similarity index 100% rename from javascripts/api/api/api.ts rename to app/javascript/legacy_react/api/api/api.ts diff --git a/javascripts/api/configuration.ts b/app/javascript/legacy_react/api/configuration.ts similarity index 100% rename from javascripts/api/configuration.ts rename to app/javascript/legacy_react/api/configuration.ts diff --git a/javascripts/api/index.ts b/app/javascript/legacy_react/api/index.ts similarity index 100% rename from javascripts/api/index.ts rename to app/javascript/legacy_react/api/index.ts diff --git a/javascripts/api/model/Nonprofit.ts b/app/javascript/legacy_react/api/model/Nonprofit.ts similarity index 100% rename from javascripts/api/model/Nonprofit.ts rename to app/javascript/legacy_react/api/model/Nonprofit.ts diff --git a/javascripts/api/model/PostNonprofit.ts b/app/javascript/legacy_react/api/model/PostNonprofit.ts similarity index 100% rename from javascripts/api/model/PostNonprofit.ts rename to app/javascript/legacy_react/api/model/PostNonprofit.ts diff --git a/javascripts/api/model/PostNonprofitNonprofit.ts b/app/javascript/legacy_react/api/model/PostNonprofitNonprofit.ts similarity index 100% rename from javascripts/api/model/PostNonprofitNonprofit.ts rename to app/javascript/legacy_react/api/model/PostNonprofitNonprofit.ts diff --git a/javascripts/api/model/PostNonprofitUser.ts b/app/javascript/legacy_react/api/model/PostNonprofitUser.ts similarity index 100% rename from javascripts/api/model/PostNonprofitUser.ts rename to app/javascript/legacy_react/api/model/PostNonprofitUser.ts diff --git a/javascripts/api/model/PostUser.ts b/app/javascript/legacy_react/api/model/PostUser.ts similarity index 100% rename from javascripts/api/model/PostUser.ts rename to app/javascript/legacy_react/api/model/PostUser.ts diff --git a/javascripts/api/model/ValidationError.ts b/app/javascript/legacy_react/api/model/ValidationError.ts similarity index 100% rename from javascripts/api/model/ValidationError.ts rename to app/javascript/legacy_react/api/model/ValidationError.ts diff --git a/javascripts/api/model/ValidationErrors.ts b/app/javascript/legacy_react/api/model/ValidationErrors.ts similarity index 100% rename from javascripts/api/model/ValidationErrors.ts rename to app/javascript/legacy_react/api/model/ValidationErrors.ts diff --git a/javascripts/api/model/models.ts b/app/javascript/legacy_react/api/model/models.ts similarity index 100% rename from javascripts/api/model/models.ts rename to app/javascript/legacy_react/api/model/models.ts diff --git a/javascripts/app/create_new_offsite_payment_pane.tsx b/app/javascript/legacy_react/app/create_new_offsite_payment_pane.tsx similarity index 100% rename from javascripts/app/create_new_offsite_payment_pane.tsx rename to app/javascript/legacy_react/app/create_new_offsite_payment_pane.tsx diff --git a/javascripts/app/edit_payment_pane.tsx b/app/javascript/legacy_react/app/edit_payment_pane.tsx similarity index 100% rename from javascripts/app/edit_payment_pane.tsx rename to app/javascript/legacy_react/app/edit_payment_pane.tsx diff --git a/javascripts/app/loading_indicator.ts b/app/javascript/legacy_react/app/loading_indicator.ts similarity index 100% rename from javascripts/app/loading_indicator.ts rename to app/javascript/legacy_react/app/loading_indicator.ts diff --git a/javascripts/app/registration_page.tsx b/app/javascript/legacy_react/app/registration_page.tsx similarity index 100% rename from javascripts/app/registration_page.tsx rename to app/javascript/legacy_react/app/registration_page.tsx diff --git a/javascripts/app/session_login_page.tsx b/app/javascript/legacy_react/app/session_login_page.tsx similarity index 100% rename from javascripts/app/session_login_page.tsx rename to app/javascript/legacy_react/app/session_login_page.tsx diff --git a/app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx b/app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx new file mode 100644 index 00000000..f3b49926 --- /dev/null +++ b/app/javascript/legacy_react/javascripts/app/create_new_offsite_payment_pane.tsx @@ -0,0 +1,33 @@ +// License: LGPL-3.0-or-later +// require a root component here. This will be treated as the root of a webpack package +import Root from "../../src/components/common/Root" +import CreateOffsitePaymentPane from "../../src/components/create_offsite_payment_pane/CreateOffsitePaymentPane" + +import * as ReactDOM from 'react-dom' +import * as React from 'react' + +export interface FundraiserInfo { + id: number + name: string +} + +function LoadReactPage(element:HTMLElement, campaigns: FundraiserInfo[], + events: FundraiserInfo[], + nonprofitId: number, + supporterId:number, + preupdateDonationAction:() => void, + postUpdateSuccess: () => void, + + //from ModalProps + onClose: () => void, + modalActive: boolean, + nonprofitTimezone?: string) { + ReactDOM.render(, element) +} + + +(window as any).LoadReactCreateOffsiteDonationPane = LoadReactPage \ No newline at end of file diff --git a/app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx b/app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx new file mode 100644 index 00000000..e5378acd --- /dev/null +++ b/app/javascript/legacy_react/javascripts/app/edit_payment_pane.tsx @@ -0,0 +1,27 @@ +// License: LGPL-3.0-or-later +// require a root component here. This will be treated as the root of a webpack package +import Root from "../../src/components/common/Root" +import EditPaymentPane, {FundraiserInfo} from "../../src/components/edit_payment_pane/EditPaymentPane" + +import * as ReactDOM from 'react-dom' +import * as React from 'react' + +function LoadReactPage(element:HTMLElement, data:any, campaigns:FundraiserInfo[], + events:FundraiserInfo[], + onClose:() => void, + modalActive:boolean, + preupdateDonationAction: () => void, + postUpdateSuccess: () => void, + nonprofitTimezone?:string + + ) { + ReactDOM.render(, element) +} + + +(window as any).LoadReactEditPaymentPane = LoadReactPage \ No newline at end of file diff --git a/app/javascript/legacy_react/javascripts/app/registration_page.tsx b/app/javascript/legacy_react/javascripts/app/registration_page.tsx new file mode 100644 index 00000000..086d632e --- /dev/null +++ b/app/javascript/legacy_react/javascripts/app/registration_page.tsx @@ -0,0 +1,16 @@ +// License: LGPL-3.0-or-later + +// require a root component here. This will be treated as the root of a webpack package +import Root from "../../src/components/common/Root" +import RegistrationPage from "../../src/components/registration_page/RegistrationPage" + +import * as ReactDOM from 'react-dom' +import * as React from 'react' + +function LoadReactPage(element:HTMLElement) { + ReactDOM.render(, element) +} + + +(window as any).LoadReactPage = LoadReactPage + diff --git a/javascripts/src/components/common/BootstrapWrapper.tsx b/app/javascript/legacy_react/src/components/common/BootstrapWrapper.tsx similarity index 100% rename from javascripts/src/components/common/BootstrapWrapper.tsx rename to app/javascript/legacy_react/src/components/common/BootstrapWrapper.tsx diff --git a/javascripts/src/components/common/DefaultCloseButton.tsx b/app/javascript/legacy_react/src/components/common/DefaultCloseButton.tsx similarity index 97% rename from javascripts/src/components/common/DefaultCloseButton.tsx rename to app/javascript/legacy_react/src/components/common/DefaultCloseButton.tsx index 2ce37e3e..0a5a3c1b 100644 --- a/javascripts/src/components/common/DefaultCloseButton.tsx +++ b/app/javascript/legacy_react/src/components/common/DefaultCloseButton.tsx @@ -1,8 +1,8 @@ -import React = require("react"); +import * as React from "react"; import { action, observable } from "mobx"; import { Transition } from "react-transition-group"; import { CloseButton } from "./svg/CloseButton"; -import color = require("color"); +import color from "color"; import { observer } from "mobx-react"; import ScreenReaderOnlyText from "./ScreenReaderOnlyText"; diff --git a/javascripts/src/components/common/LabeledFieldComponent.spec.tsx b/app/javascript/legacy_react/src/components/common/LabeledFieldComponent.spec.tsx similarity index 100% rename from javascripts/src/components/common/LabeledFieldComponent.spec.tsx rename to app/javascript/legacy_react/src/components/common/LabeledFieldComponent.spec.tsx diff --git a/javascripts/src/components/common/LabeledFieldComponent.tsx b/app/javascript/legacy_react/src/components/common/LabeledFieldComponent.tsx similarity index 100% rename from javascripts/src/components/common/LabeledFieldComponent.tsx rename to app/javascript/legacy_react/src/components/common/LabeledFieldComponent.tsx diff --git a/javascripts/src/components/common/Modal.spec.tsx b/app/javascript/legacy_react/src/components/common/Modal.spec.tsx similarity index 100% rename from javascripts/src/components/common/Modal.spec.tsx rename to app/javascript/legacy_react/src/components/common/Modal.spec.tsx diff --git a/javascripts/src/components/common/Modal.tsx b/app/javascript/legacy_react/src/components/common/Modal.tsx similarity index 97% rename from javascripts/src/components/common/Modal.tsx rename to app/javascript/legacy_react/src/components/common/Modal.tsx index ceb437d5..1c3fae1e 100644 --- a/javascripts/src/components/common/Modal.tsx +++ b/app/javascript/legacy_react/src/components/common/Modal.tsx @@ -1,7 +1,7 @@ // License: LGPL-3.0-or-later import * as React from 'react'; import { observer } from 'mobx-react'; -import AriaModal = require('react-aria-modal'); +import AriaModal from 'react-aria-modal'; import { DefaultCloseButton } from './DefaultCloseButton'; import BootstrapWrapper from './BootstrapWrapper'; import { Row, Column } from './layout'; diff --git a/javascripts/src/components/common/ProgressableButton.spec.tsx b/app/javascript/legacy_react/src/components/common/ProgressableButton.spec.tsx similarity index 100% rename from javascripts/src/components/common/ProgressableButton.spec.tsx rename to app/javascript/legacy_react/src/components/common/ProgressableButton.spec.tsx diff --git a/javascripts/src/components/common/ProgressableButton.tsx b/app/javascript/legacy_react/src/components/common/ProgressableButton.tsx similarity index 100% rename from javascripts/src/components/common/ProgressableButton.tsx rename to app/javascript/legacy_react/src/components/common/ProgressableButton.tsx diff --git a/javascripts/src/components/common/Root.tsx b/app/javascript/legacy_react/src/components/common/Root.tsx similarity index 95% rename from javascripts/src/components/common/Root.tsx rename to app/javascript/legacy_react/src/components/common/Root.tsx index 36021130..ce4696ff 100644 --- a/javascripts/src/components/common/Root.tsx +++ b/app/javascript/legacy_react/src/components/common/Root.tsx @@ -10,7 +10,7 @@ import {CSRFInterceptor} from "../../lib/csrf_interceptor"; import * as CustomAPIS from "../../lib/apis" const enLocaleData = require('react-intl/locale-data/en'); -const I18n = require('../../../../app/javascript/i18n.js.erb') +const I18n = require('../../../../i18n.js.erb') const localeData = [...enLocaleData] Object.keys(I18n.translations).filter((i:string) => i !== 'en').forEach((i:string) => { const data = [...require(`react-intl/locale-data/${i}`)] diff --git a/javascripts/src/components/common/ScreenReaderOnlyText.spec.tsx b/app/javascript/legacy_react/src/components/common/ScreenReaderOnlyText.spec.tsx similarity index 100% rename from javascripts/src/components/common/ScreenReaderOnlyText.spec.tsx rename to app/javascript/legacy_react/src/components/common/ScreenReaderOnlyText.spec.tsx diff --git a/javascripts/src/components/common/ScreenReaderOnlyText.tsx b/app/javascript/legacy_react/src/components/common/ScreenReaderOnlyText.tsx similarity index 100% rename from javascripts/src/components/common/ScreenReaderOnlyText.tsx rename to app/javascript/legacy_react/src/components/common/ScreenReaderOnlyText.tsx diff --git a/javascripts/src/components/common/Spinner.spec.tsx b/app/javascript/legacy_react/src/components/common/Spinner.spec.tsx similarity index 100% rename from javascripts/src/components/common/Spinner.spec.tsx rename to app/javascript/legacy_react/src/components/common/Spinner.spec.tsx diff --git a/javascripts/src/components/common/Spinner.tsx b/app/javascript/legacy_react/src/components/common/Spinner.tsx similarity index 100% rename from javascripts/src/components/common/Spinner.tsx rename to app/javascript/legacy_react/src/components/common/Spinner.tsx diff --git a/javascripts/src/components/common/StandardFieldComponent.spec.tsx b/app/javascript/legacy_react/src/components/common/StandardFieldComponent.spec.tsx similarity index 100% rename from javascripts/src/components/common/StandardFieldComponent.spec.tsx rename to app/javascript/legacy_react/src/components/common/StandardFieldComponent.spec.tsx diff --git a/javascripts/src/components/common/StandardFieldComponent.tsx b/app/javascript/legacy_react/src/components/common/StandardFieldComponent.tsx similarity index 100% rename from javascripts/src/components/common/StandardFieldComponent.tsx rename to app/javascript/legacy_react/src/components/common/StandardFieldComponent.tsx diff --git a/javascripts/src/components/common/__snapshots__/LabeledFieldComponent.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/LabeledFieldComponent.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/LabeledFieldComponent.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/LabeledFieldComponent.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/Modal.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/Modal.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/Modal.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/Modal.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/ProgressableButton.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/ProgressableButton.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/ProgressableButton.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/ProgressableButton.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/ScreenReaderOnlyText.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/ScreenReaderOnlyText.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/ScreenReaderOnlyText.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/ScreenReaderOnlyText.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/Spinner.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/Spinner.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/Spinner.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/Spinner.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/StandardFieldComponent.spec.tsx.snap diff --git a/javascripts/src/components/common/__snapshots__/layout.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/__snapshots__/layout.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/__snapshots__/layout.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/__snapshots__/layout.spec.tsx.snap diff --git a/javascripts/src/components/common/fields.tsx b/app/javascript/legacy_react/src/components/common/fields.tsx similarity index 98% rename from javascripts/src/components/common/fields.tsx rename to app/javascript/legacy_react/src/components/common/fields.tsx index fa8be668..fe151380 100644 --- a/javascripts/src/components/common/fields.tsx +++ b/app/javascript/legacy_react/src/components/common/fields.tsx @@ -1,7 +1,7 @@ // License: LGPL-3.0-or-later import * as React from 'react'; import { observer } from "mobx-react"; -import { Field } from "../../../../types/mobx-react-form"; +import { Field } from "../../../../../../types/mobx-react-form"; import LabeledFieldComponent from "./LabeledFieldComponent"; import { HoudiniField } from "../../lib/houdini_form"; import ReactInput from "./form/ReactInput"; diff --git a/javascripts/src/components/common/form/ReactForm.tsx b/app/javascript/legacy_react/src/components/common/form/ReactForm.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactForm.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactForm.tsx diff --git a/javascripts/src/components/common/form/ReactInput.spec.tsx b/app/javascript/legacy_react/src/components/common/form/ReactInput.spec.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactInput.spec.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactInput.spec.tsx diff --git a/javascripts/src/components/common/form/ReactInput.tsx b/app/javascript/legacy_react/src/components/common/form/ReactInput.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactInput.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactInput.tsx diff --git a/javascripts/src/components/common/form/ReactMaskedInput.tsx b/app/javascript/legacy_react/src/components/common/form/ReactMaskedInput.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactMaskedInput.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactMaskedInput.tsx diff --git a/javascripts/src/components/common/form/ReactSelect.spec.tsx b/app/javascript/legacy_react/src/components/common/form/ReactSelect.spec.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactSelect.spec.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactSelect.spec.tsx diff --git a/javascripts/src/components/common/form/ReactSelect.tsx b/app/javascript/legacy_react/src/components/common/form/ReactSelect.tsx similarity index 96% rename from javascripts/src/components/common/form/ReactSelect.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactSelect.tsx index acba6f6e..c3e0bb42 100644 --- a/javascripts/src/components/common/form/ReactSelect.tsx +++ b/app/javascript/legacy_react/src/components/common/form/ReactSelect.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import {InjectedIntlProps, injectIntl} from 'react-intl'; -import {Field} from "../../../../../types/mobx-react-form"; +import {Field} from "../../../../../../../types/mobx-react-form"; import {InputHTMLAttributes} from "react"; import {action, observable} from "mobx"; import {SelectHTMLAttributes} from "react"; diff --git a/javascripts/src/components/common/form/ReactTextarea.spec.tsx b/app/javascript/legacy_react/src/components/common/form/ReactTextarea.spec.tsx similarity index 100% rename from javascripts/src/components/common/form/ReactTextarea.spec.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactTextarea.spec.tsx diff --git a/javascripts/src/components/common/form/ReactTextarea.tsx b/app/javascript/legacy_react/src/components/common/form/ReactTextarea.tsx similarity index 95% rename from javascripts/src/components/common/form/ReactTextarea.tsx rename to app/javascript/legacy_react/src/components/common/form/ReactTextarea.tsx index a948a2af..3e8a826a 100644 --- a/javascripts/src/components/common/form/ReactTextarea.tsx +++ b/app/javascript/legacy_react/src/components/common/form/ReactTextarea.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import {InjectedIntlProps, injectIntl} from 'react-intl'; -import {Field} from "../../../../../types/mobx-react-form"; +import {Field} from "../../../../../../../types/mobx-react-form"; import {InputHTMLAttributes, ReactText, TextareaHTMLAttributes} from "react"; import {action, observable} from "mobx"; import {ReactInputProps} from "./react_input_props"; diff --git a/javascripts/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactInput.spec.tsx.snap diff --git a/javascripts/src/components/common/form/__snapshots__/ReactSelect.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactSelect.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/form/__snapshots__/ReactSelect.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactSelect.spec.tsx.snap diff --git a/javascripts/src/components/common/form/__snapshots__/ReactTextarea.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactTextarea.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/form/__snapshots__/ReactTextarea.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/form/__snapshots__/ReactTextarea.spec.tsx.snap diff --git a/javascripts/src/components/common/form/react_input_props.ts b/app/javascript/legacy_react/src/components/common/form/react_input_props.ts similarity index 100% rename from javascripts/src/components/common/form/react_input_props.ts rename to app/javascript/legacy_react/src/components/common/form/react_input_props.ts diff --git a/javascripts/src/components/common/layout.spec.tsx b/app/javascript/legacy_react/src/components/common/layout.spec.tsx similarity index 100% rename from javascripts/src/components/common/layout.spec.tsx rename to app/javascript/legacy_react/src/components/common/layout.spec.tsx diff --git a/javascripts/src/components/common/layout.tsx b/app/javascript/legacy_react/src/components/common/layout.tsx similarity index 100% rename from javascripts/src/components/common/layout.tsx rename to app/javascript/legacy_react/src/components/common/layout.tsx diff --git a/javascripts/src/components/common/selectable_table_row/SelectableTableRow.spec.tsx b/app/javascript/legacy_react/src/components/common/selectable_table_row/SelectableTableRow.spec.tsx similarity index 100% rename from javascripts/src/components/common/selectable_table_row/SelectableTableRow.spec.tsx rename to app/javascript/legacy_react/src/components/common/selectable_table_row/SelectableTableRow.spec.tsx diff --git a/javascripts/src/components/common/selectable_table_row/SelectableTableRow.tsx b/app/javascript/legacy_react/src/components/common/selectable_table_row/SelectableTableRow.tsx similarity index 100% rename from javascripts/src/components/common/selectable_table_row/SelectableTableRow.tsx rename to app/javascript/legacy_react/src/components/common/selectable_table_row/SelectableTableRow.tsx diff --git a/javascripts/src/components/common/selectable_table_row/connect.tsx b/app/javascript/legacy_react/src/components/common/selectable_table_row/connect.tsx similarity index 100% rename from javascripts/src/components/common/selectable_table_row/connect.tsx rename to app/javascript/legacy_react/src/components/common/selectable_table_row/connect.tsx diff --git a/javascripts/src/components/common/svg/CloseButton.tsx b/app/javascript/legacy_react/src/components/common/svg/CloseButton.tsx similarity index 96% rename from javascripts/src/components/common/svg/CloseButton.tsx rename to app/javascript/legacy_react/src/components/common/svg/CloseButton.tsx index d8e23f00..5167c93c 100644 --- a/javascripts/src/components/common/svg/CloseButton.tsx +++ b/app/javascript/legacy_react/src/components/common/svg/CloseButton.tsx @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later -import React = require("react"); +import * as React from "react"; interface CloseButtonProps { backgroundCircleStyle:React.CSSProperties foregroundCircleStyle:React.CSSProperties diff --git a/javascripts/src/components/common/svg/checkbox.tsx b/app/javascript/legacy_react/src/components/common/svg/checkbox.tsx similarity index 100% rename from javascripts/src/components/common/svg/checkbox.tsx rename to app/javascript/legacy_react/src/components/common/svg/checkbox.tsx diff --git a/javascripts/src/components/common/test/react_test_helpers.tsx b/app/javascript/legacy_react/src/components/common/test/react_test_helpers.tsx similarity index 100% rename from javascripts/src/components/common/test/react_test_helpers.tsx rename to app/javascript/legacy_react/src/components/common/test/react_test_helpers.tsx diff --git a/javascripts/src/components/common/test/unique_id_mock.ts b/app/javascript/legacy_react/src/components/common/test/unique_id_mock.ts similarity index 100% rename from javascripts/src/components/common/test/unique_id_mock.ts rename to app/javascript/legacy_react/src/components/common/test/unique_id_mock.ts diff --git a/javascripts/src/components/common/wizard/RAT/Tab.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/Tab.ts similarity index 100% rename from javascripts/src/components/common/wizard/RAT/Tab.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/Tab.ts diff --git a/javascripts/src/components/common/wizard/RAT/TabList.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/TabList.ts similarity index 100% rename from javascripts/src/components/common/wizard/RAT/TabList.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/TabList.ts diff --git a/javascripts/src/components/common/wizard/RAT/TabPanel.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/TabPanel.ts similarity index 100% rename from javascripts/src/components/common/wizard/RAT/TabPanel.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/TabPanel.ts diff --git a/javascripts/src/components/common/wizard/RAT/Wrapper.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/RAT/Wrapper.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/RAT/Wrapper.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/RAT/Wrapper.spec.tsx diff --git a/javascripts/src/components/common/wizard/RAT/Wrapper.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/Wrapper.ts similarity index 96% rename from javascripts/src/components/common/wizard/RAT/Wrapper.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/Wrapper.ts index 54f0f9d8..66f1e3d5 100644 --- a/javascripts/src/components/common/wizard/RAT/Wrapper.ts +++ b/app/javascript/legacy_react/src/components/common/wizard/RAT/Wrapper.ts @@ -5,7 +5,7 @@ import {TabManagerParent} from "./abstract_tabcomponent_state"; import {observer} from 'mobx-react'; import specialAssign from "./specialAssign"; -import PropTypes = require('prop-types'); +const PropTypes = require ('prop-types'); interface WrapperProps { manager: TabManagerParent diff --git a/javascripts/src/components/common/wizard/RAT/__snapshots__/Wrapper.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/wizard/RAT/__snapshots__/Wrapper.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/wizard/RAT/__snapshots__/Wrapper.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/wizard/RAT/__snapshots__/Wrapper.spec.tsx.snap diff --git a/javascripts/src/components/common/wizard/RAT/abstract_tabcomponent_state.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/RAT/abstract_tabcomponent_state.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/RAT/abstract_tabcomponent_state.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/RAT/abstract_tabcomponent_state.spec.tsx diff --git a/javascripts/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts similarity index 99% rename from javascripts/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts index d55d3b89..c3c6cd3d 100644 --- a/javascripts/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts +++ b/app/javascript/legacy_react/src/components/common/wizard/RAT/abstract_tabcomponent_state.ts @@ -1,6 +1,6 @@ // License: LGPL-3.0-or-later import {action, computed, observable, reaction, runInAction} from "mobx"; -import _ = require("lodash"); +import * as _ from "lodash"; const createFocusGroup = require('focus-group'); diff --git a/javascripts/src/components/common/wizard/RAT/specialAssign.ts b/app/javascript/legacy_react/src/components/common/wizard/RAT/specialAssign.ts similarity index 100% rename from javascripts/src/components/common/wizard/RAT/specialAssign.ts rename to app/javascript/legacy_react/src/components/common/wizard/RAT/specialAssign.ts diff --git a/javascripts/src/components/common/wizard/Wizard.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/Wizard.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/Wizard.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/Wizard.spec.tsx diff --git a/javascripts/src/components/common/wizard/Wizard.tsx b/app/javascript/legacy_react/src/components/common/wizard/Wizard.tsx similarity index 100% rename from javascripts/src/components/common/wizard/Wizard.tsx rename to app/javascript/legacy_react/src/components/common/wizard/Wizard.tsx diff --git a/javascripts/src/components/common/wizard/WizardPanel.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/WizardPanel.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/WizardPanel.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/WizardPanel.spec.tsx diff --git a/javascripts/src/components/common/wizard/WizardPanel.tsx b/app/javascript/legacy_react/src/components/common/wizard/WizardPanel.tsx similarity index 100% rename from javascripts/src/components/common/wizard/WizardPanel.tsx rename to app/javascript/legacy_react/src/components/common/wizard/WizardPanel.tsx diff --git a/javascripts/src/components/common/wizard/WizardTab.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/WizardTab.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/WizardTab.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/WizardTab.spec.tsx diff --git a/javascripts/src/components/common/wizard/WizardTab.tsx b/app/javascript/legacy_react/src/components/common/wizard/WizardTab.tsx similarity index 100% rename from javascripts/src/components/common/wizard/WizardTab.tsx rename to app/javascript/legacy_react/src/components/common/wizard/WizardTab.tsx diff --git a/javascripts/src/components/common/wizard/WizardTabList.tsx b/app/javascript/legacy_react/src/components/common/wizard/WizardTabList.tsx similarity index 100% rename from javascripts/src/components/common/wizard/WizardTabList.tsx rename to app/javascript/legacy_react/src/components/common/wizard/WizardTabList.tsx diff --git a/javascripts/src/components/common/wizard/__snapshots__/Wizard.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/wizard/__snapshots__/Wizard.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/wizard/__snapshots__/Wizard.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/wizard/__snapshots__/Wizard.spec.tsx.snap diff --git a/javascripts/src/components/common/wizard/__snapshots__/WizardPanel.spec.tsx.snap b/app/javascript/legacy_react/src/components/common/wizard/__snapshots__/WizardPanel.spec.tsx.snap similarity index 100% rename from javascripts/src/components/common/wizard/__snapshots__/WizardPanel.spec.tsx.snap rename to app/javascript/legacy_react/src/components/common/wizard/__snapshots__/WizardPanel.spec.tsx.snap diff --git a/javascripts/src/components/common/wizard/abstract_wizard_state.spec.tsx b/app/javascript/legacy_react/src/components/common/wizard/abstract_wizard_state.spec.tsx similarity index 100% rename from javascripts/src/components/common/wizard/abstract_wizard_state.spec.tsx rename to app/javascript/legacy_react/src/components/common/wizard/abstract_wizard_state.spec.tsx diff --git a/javascripts/src/components/common/wizard/abstract_wizard_state.ts b/app/javascript/legacy_react/src/components/common/wizard/abstract_wizard_state.ts similarity index 98% rename from javascripts/src/components/common/wizard/abstract_wizard_state.ts rename to app/javascript/legacy_react/src/components/common/wizard/abstract_wizard_state.ts index a6ab3c78..ab5a05f4 100644 --- a/javascripts/src/components/common/wizard/abstract_wizard_state.ts +++ b/app/javascript/legacy_react/src/components/common/wizard/abstract_wizard_state.ts @@ -1,7 +1,7 @@ // License: LGPL-3.0-or-later import {computed, reaction} from "mobx"; import {AbstractTabComponentState, AbstractTabPanelState} from "./RAT/abstract_tabcomponent_state"; -import _ = require("lodash"); +import * as _ from "lodash"; export abstract class AbstractWizardState extends AbstractTabComponentState { diff --git a/javascripts/src/components/common/wizard/wizard_state.spec.ts b/app/javascript/legacy_react/src/components/common/wizard/wizard_state.spec.ts similarity index 100% rename from javascripts/src/components/common/wizard/wizard_state.spec.ts rename to app/javascript/legacy_react/src/components/common/wizard/wizard_state.spec.ts diff --git a/javascripts/src/components/common/wizard/wizard_state.ts b/app/javascript/legacy_react/src/components/common/wizard/wizard_state.ts similarity index 99% rename from javascripts/src/components/common/wizard/wizard_state.ts rename to app/javascript/legacy_react/src/components/common/wizard/wizard_state.ts index 843baa39..5b6b383f 100644 --- a/javascripts/src/components/common/wizard/wizard_state.ts +++ b/app/javascript/legacy_react/src/components/common/wizard/wizard_state.ts @@ -1,7 +1,7 @@ // License: LGPL-3.0-or-later import {observable, action, computed, toJS, reaction, runInAction} from "mobx"; import {Field, Form, FieldDefinition, FieldHandlers, FieldHooks} from "mobx-react-form"; -import _ = require("lodash"); +import * as _ from "lodash"; import {AbstractWizardState, AbstractWizardTabPanelState} from "./abstract_wizard_state"; interface SubFormDefinition { diff --git a/javascripts/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx b/app/javascript/legacy_react/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx similarity index 98% rename from javascripts/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx rename to app/javascript/legacy_react/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx index 169d9452..751bbbbd 100644 --- a/javascripts/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx +++ b/app/javascript/legacy_react/src/components/create_offsite_payment_pane/CreateOffsitePaymentPane.tsx @@ -9,7 +9,7 @@ import {BasicField, CurrencyField, SelectField, TextareaField} from "../common/f import ProgressableButton from "../common/ProgressableButton"; import {action, computed} from "mobx"; import {NonprofitTimezonedDates} from "../../lib/date"; -import {Field, FieldDefinition} from "../../../../types/mobx-react-form"; +import {Field, FieldDefinition} from "../../../../../../types/mobx-react-form"; import {createFieldDefinition} from "../../lib/mobx_utils"; import {centsToDollars, dollarsToCents} from "../../lib/format"; import {Validations} from "../../lib/vjf_rules"; @@ -18,9 +18,9 @@ import {ApiManager} from "../../lib/api_manager"; import * as CustomAPIS from "../../lib/apis"; import {CSRFInterceptor} from "../../lib/csrf_interceptor"; import {CreateOffsiteDonation, CreateOffsiteDonationModel} from "../../lib/api/create_offsite_donation"; -import blacklist = require("validator/lib/blacklist"); -import * as _ from 'lodash' -import moment = require('moment'); +import blacklist from "validator/lib/blacklist"; +import * as _ from 'lodash'; +import moment from 'moment'; import { castToUndefinedIfBlank } from '../../lib/utils'; import ReactInput from "../common/form/ReactInput"; diff --git a/javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx b/app/javascript/legacy_react/src/components/edit_payment_pane/EditPaymentPane.tsx similarity index 99% rename from javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx rename to app/javascript/legacy_react/src/components/edit_payment_pane/EditPaymentPane.tsx index 6b54a46b..ce9d0260 100644 --- a/javascripts/src/components/edit_payment_pane/EditPaymentPane.tsx +++ b/app/javascript/legacy_react/src/components/edit_payment_pane/EditPaymentPane.tsx @@ -15,9 +15,9 @@ import {CSRFInterceptor} from "../../lib/csrf_interceptor"; import {BasicField, CurrencyField, SelectField, TextareaField} from '../common/fields'; import {TwoColumnFields} from "../common/layout"; import {Validations} from "../../lib/vjf_rules"; -import _ = require("lodash"); +import * as _ from 'lodash' import {Dedication, parseDedication, serializeDedication} from '../../lib/dedication'; -import blacklist = require("validator/lib/blacklist"); +import blacklist from "validator/lib/blacklist"; import {createFieldDefinition} from "../../lib/mobx_utils"; import Modal from "../common/Modal"; import ReactInput from "../common/form/ReactInput"; diff --git a/javascripts/src/components/registration_page/NonprofitInfoForm.spec.tsx b/app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.spec.tsx similarity index 100% rename from javascripts/src/components/registration_page/NonprofitInfoForm.spec.tsx rename to app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.spec.tsx diff --git a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx b/app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.tsx similarity index 98% rename from javascripts/src/components/registration_page/NonprofitInfoForm.tsx rename to app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.tsx index b82bc1bc..51dc4914 100644 --- a/javascripts/src/components/registration_page/NonprofitInfoForm.tsx +++ b/app/javascript/legacy_react/src/components/registration_page/NonprofitInfoForm.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { observer } from 'mobx-react'; import {InjectedIntlProps, injectIntl} from 'react-intl'; -import {Field, FieldDefinition} from "../../../../types/mobx-react-form"; +import {Field, FieldDefinition} from "../../../../../../types/mobx-react-form"; import {BasicField} from "../common/fields"; import {ThreeColumnFields, TwoColumnFields} from "../common/layout"; import {Validations} from "../../lib/vjf_rules"; diff --git a/javascripts/src/components/registration_page/NonprofitInfoPanel.spec.tsx b/app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.spec.tsx similarity index 100% rename from javascripts/src/components/registration_page/NonprofitInfoPanel.spec.tsx rename to app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.spec.tsx diff --git a/javascripts/src/components/registration_page/NonprofitInfoPanel.tsx b/app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.tsx similarity index 100% rename from javascripts/src/components/registration_page/NonprofitInfoPanel.tsx rename to app/javascript/legacy_react/src/components/registration_page/NonprofitInfoPanel.tsx diff --git a/javascripts/src/components/registration_page/RegistrationPage.tsx b/app/javascript/legacy_react/src/components/registration_page/RegistrationPage.tsx similarity index 100% rename from javascripts/src/components/registration_page/RegistrationPage.tsx rename to app/javascript/legacy_react/src/components/registration_page/RegistrationPage.tsx diff --git a/javascripts/src/components/registration_page/RegistrationWizard.tsx b/app/javascript/legacy_react/src/components/registration_page/RegistrationWizard.tsx similarity index 98% rename from javascripts/src/components/registration_page/RegistrationWizard.tsx rename to app/javascript/legacy_react/src/components/registration_page/RegistrationWizard.tsx index 3eef62fd..2303da37 100644 --- a/javascripts/src/components/registration_page/RegistrationWizard.tsx +++ b/app/javascript/legacy_react/src/components/registration_page/RegistrationWizard.tsx @@ -20,7 +20,7 @@ import { PostNonprofitUser } from "../../../api"; -import {initializationDefinition} from "../../../../types/mobx-react-form"; +import {initializationDefinition} from "../../../../../../types/mobx-react-form"; import {ApiManager} from "../../lib/api_manager"; import {HoudiniForm, StaticFormToErrorAndBackConverter} from "../../lib/houdini_form"; import {WebUserSignInOut} from "../../lib/api/sign_in"; diff --git a/javascripts/src/components/registration_page/UserInfoForm.tsx b/app/javascript/legacy_react/src/components/registration_page/UserInfoForm.tsx similarity index 100% rename from javascripts/src/components/registration_page/UserInfoForm.tsx rename to app/javascript/legacy_react/src/components/registration_page/UserInfoForm.tsx diff --git a/javascripts/src/components/registration_page/UserInfoPanel.spec.tsx b/app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.spec.tsx similarity index 100% rename from javascripts/src/components/registration_page/UserInfoPanel.spec.tsx rename to app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.spec.tsx diff --git a/javascripts/src/components/registration_page/UserInfoPanel.tsx b/app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.tsx similarity index 100% rename from javascripts/src/components/registration_page/UserInfoPanel.tsx rename to app/javascript/legacy_react/src/components/registration_page/UserInfoPanel.tsx diff --git a/javascripts/src/components/session_login_page/SessionLoginForm.tsx b/app/javascript/legacy_react/src/components/session_login_page/SessionLoginForm.tsx similarity index 98% rename from javascripts/src/components/session_login_page/SessionLoginForm.tsx rename to app/javascript/legacy_react/src/components/session_login_page/SessionLoginForm.tsx index 3d7d3ca8..d098a8ce 100644 --- a/javascripts/src/components/session_login_page/SessionLoginForm.tsx +++ b/app/javascript/legacy_react/src/components/session_login_page/SessionLoginForm.tsx @@ -2,7 +2,7 @@ import * as React from 'react'; import { observer, inject} from 'mobx-react'; import {InjectedIntlProps, injectIntl, FormattedMessage} from 'react-intl'; -import {Field, FieldDefinition, Form, initializationDefinition} from "../../../../types/mobx-react-form"; +import {Field, FieldDefinition, Form, initializationDefinition} from "../../../../../../types/mobx-react-form"; import {Validations} from "../../lib/vjf_rules"; import {WebLoginModel, WebUserSignInOut} from "../../lib/api/sign_in"; diff --git a/javascripts/src/components/session_login_page/SessionLoginPage.tsx b/app/javascript/legacy_react/src/components/session_login_page/SessionLoginPage.tsx similarity index 100% rename from javascripts/src/components/session_login_page/SessionLoginPage.tsx rename to app/javascript/legacy_react/src/components/session_login_page/SessionLoginPage.tsx diff --git a/javascripts/src/lib/api/create_offsite_donation.ts b/app/javascript/legacy_react/src/lib/api/create_offsite_donation.ts similarity index 93% rename from javascripts/src/lib/api/create_offsite_donation.ts rename to app/javascript/legacy_react/src/lib/api/create_offsite_donation.ts index 98ae49e1..d54ba985 100644 --- a/javascripts/src/lib/api/create_offsite_donation.ts +++ b/app/javascript/legacy_react/src/lib/api/create_offsite_donation.ts @@ -58,11 +58,11 @@ export class CreateOffsiteDonation { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); diff --git a/javascripts/src/lib/api/put_donation.ts b/app/javascript/legacy_react/src/lib/api/put_donation.ts similarity index 93% rename from javascripts/src/lib/api/put_donation.ts rename to app/javascript/legacy_react/src/lib/api/put_donation.ts index 86d30407..e573f113 100644 --- a/javascripts/src/lib/api/put_donation.ts +++ b/app/javascript/legacy_react/src/lib/api/put_donation.ts @@ -58,11 +58,11 @@ export class PutDonation { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); diff --git a/javascripts/src/lib/api/sign_in.ts b/app/javascript/legacy_react/src/lib/api/sign_in.ts similarity index 92% rename from javascripts/src/lib/api/sign_in.ts rename to app/javascript/legacy_react/src/lib/api/sign_in.ts index 0a82e0c8..e5fb4724 100644 --- a/javascripts/src/lib/api/sign_in.ts +++ b/app/javascript/legacy_react/src/lib/api/sign_in.ts @@ -58,11 +58,11 @@ export class WebUserSignInOut { } if (extraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, extraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, extraJQueryAjaxSettings); } if (this.defaultExtraJQueryAjaxSettings) { - requestOptions = (Object).assign(requestOptions, this.defaultExtraJQueryAjaxSettings); + requestOptions = Object.assign(requestOptions, this.defaultExtraJQueryAjaxSettings); } let dfd = $.Deferred(); diff --git a/javascripts/src/lib/api_manager.spec.ts b/app/javascript/legacy_react/src/lib/api_manager.spec.ts similarity index 100% rename from javascripts/src/lib/api_manager.spec.ts rename to app/javascript/legacy_react/src/lib/api_manager.spec.ts diff --git a/javascripts/src/lib/api_manager.ts b/app/javascript/legacy_react/src/lib/api_manager.ts similarity index 94% rename from javascripts/src/lib/api_manager.ts rename to app/javascript/legacy_react/src/lib/api_manager.ts index 512fa6d6..c15aa3eb 100644 --- a/javascripts/src/lib/api_manager.ts +++ b/app/javascript/legacy_react/src/lib/api_manager.ts @@ -27,10 +27,10 @@ export class ApiManager { let newed = new i() if (beforeSendInterceptors && beforeSendInterceptors.length > 0) { let a: JQuery.AjaxSettings = { - beforeSend: ((jqXHR:JQuery.jqXHR, settings:JQuery.AjaxSettings) : false|void => { + beforeSend: ((jqXHR:JQuery.jqXHR, settings:JQuery.AjaxSettings) : false|void => { _.forEach(beforeSendInterceptors, (i:Interceptor) => i(jqXHR, settings)) return - }) + }) as any } newed.defaultExtraJQueryAjaxSettings = a } diff --git a/javascripts/src/lib/apis.ts b/app/javascript/legacy_react/src/lib/apis.ts similarity index 100% rename from javascripts/src/lib/apis.ts rename to app/javascript/legacy_react/src/lib/apis.ts diff --git a/javascripts/src/lib/createNumberMask.spec.ts b/app/javascript/legacy_react/src/lib/createNumberMask.spec.ts similarity index 100% rename from javascripts/src/lib/createNumberMask.spec.ts rename to app/javascript/legacy_react/src/lib/createNumberMask.spec.ts diff --git a/javascripts/src/lib/createNumberMask.ts b/app/javascript/legacy_react/src/lib/createNumberMask.ts similarity index 100% rename from javascripts/src/lib/createNumberMask.ts rename to app/javascript/legacy_react/src/lib/createNumberMask.ts diff --git a/javascripts/src/lib/csrf_interceptor.ts b/app/javascript/legacy_react/src/lib/csrf_interceptor.ts similarity index 83% rename from javascripts/src/lib/csrf_interceptor.ts rename to app/javascript/legacy_react/src/lib/csrf_interceptor.ts index dea0db9c..ff129012 100644 --- a/javascripts/src/lib/csrf_interceptor.ts +++ b/app/javascript/legacy_react/src/lib/csrf_interceptor.ts @@ -6,6 +6,6 @@ * @returns {false | void} */ export function CSRFInterceptor(this:any, jqXHR:JQuery.jqXHR, settings: JQuery.AjaxSettings): false|void { - jqXHR.setRequestHeader('X-CSRF-Token', (window)._csrf) + jqXHR.setRequestHeader('X-CSRF-Token', (window as any)._csrf) } diff --git a/javascripts/src/lib/date.ts b/app/javascript/legacy_react/src/lib/date.ts similarity index 98% rename from javascripts/src/lib/date.ts rename to app/javascript/legacy_react/src/lib/date.ts index 86ecdb7d..98695365 100644 --- a/javascripts/src/lib/date.ts +++ b/app/javascript/legacy_react/src/lib/date.ts @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later -import * as moment from 'moment'; +import moment from 'moment'; import 'moment-timezone' function momentTz(date:string, timezone:string='UTC'):moment.Moment { diff --git a/javascripts/src/lib/dedication.ts b/app/javascript/legacy_react/src/lib/dedication.ts similarity index 100% rename from javascripts/src/lib/dedication.ts rename to app/javascript/legacy_react/src/lib/dedication.ts diff --git a/javascripts/src/lib/deprecated_format.ts b/app/javascript/legacy_react/src/lib/deprecated_format.ts similarity index 100% rename from javascripts/src/lib/deprecated_format.ts rename to app/javascript/legacy_react/src/lib/deprecated_format.ts diff --git a/javascripts/src/lib/format.spec.ts b/app/javascript/legacy_react/src/lib/format.spec.ts similarity index 100% rename from javascripts/src/lib/format.spec.ts rename to app/javascript/legacy_react/src/lib/format.spec.ts diff --git a/javascripts/src/lib/format.ts b/app/javascript/legacy_react/src/lib/format.ts similarity index 100% rename from javascripts/src/lib/format.ts rename to app/javascript/legacy_react/src/lib/format.ts diff --git a/javascripts/src/lib/houdini_form.ts b/app/javascript/legacy_react/src/lib/houdini_form.ts similarity index 99% rename from javascripts/src/lib/houdini_form.ts rename to app/javascript/legacy_react/src/lib/houdini_form.ts index a40c2cbd..726d7a15 100644 --- a/javascripts/src/lib/houdini_form.ts +++ b/app/javascript/legacy_react/src/lib/houdini_form.ts @@ -3,7 +3,7 @@ import {Field, FieldDefinition, Form, initializationDefinition} from "mobx-react import {action, computed, IValueDidChange, observable, runInAction} from 'mobx' import * as _ from 'lodash' import {ValidationErrorsException} from "../../api"; -import validator = require("validator"); +import validator from "validator"; export class HoudiniForm extends Form { diff --git a/javascripts/src/lib/mobx_utils.ts b/app/javascript/legacy_react/src/lib/mobx_utils.ts similarity index 100% rename from javascripts/src/lib/mobx_utils.ts rename to app/javascript/legacy_react/src/lib/mobx_utils.ts diff --git a/javascripts/src/lib/nonprofitBranding.ts b/app/javascript/legacy_react/src/lib/nonprofitBranding.ts similarity index 95% rename from javascripts/src/lib/nonprofitBranding.ts rename to app/javascript/legacy_react/src/lib/nonprofitBranding.ts index b05f5d43..d9ee335c 100644 --- a/javascripts/src/lib/nonprofitBranding.ts +++ b/app/javascript/legacy_react/src/lib/nonprofitBranding.ts @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later -import color = require('color') +import color from 'color'; import { Color } from 'csstype'; interface CustomBrandColors { diff --git a/javascripts/src/lib/payments/credit_card.spec.ts b/app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts similarity index 100% rename from javascripts/src/lib/payments/credit_card.spec.ts rename to app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts diff --git a/javascripts/src/lib/payments/credit_card.ts b/app/javascript/legacy_react/src/lib/payments/credit_card.ts similarity index 100% rename from javascripts/src/lib/payments/credit_card.ts rename to app/javascript/legacy_react/src/lib/payments/credit_card.ts diff --git a/javascripts/src/lib/regex.spec.ts b/app/javascript/legacy_react/src/lib/regex.spec.ts similarity index 100% rename from javascripts/src/lib/regex.spec.ts rename to app/javascript/legacy_react/src/lib/regex.spec.ts diff --git a/javascripts/src/lib/regex.ts b/app/javascript/legacy_react/src/lib/regex.ts similarity index 100% rename from javascripts/src/lib/regex.ts rename to app/javascript/legacy_react/src/lib/regex.ts diff --git a/javascripts/src/lib/tests/helpers.ts b/app/javascript/legacy_react/src/lib/tests/helpers.ts similarity index 100% rename from javascripts/src/lib/tests/helpers.ts rename to app/javascript/legacy_react/src/lib/tests/helpers.ts diff --git a/javascripts/src/lib/utils.ts b/app/javascript/legacy_react/src/lib/utils.ts similarity index 100% rename from javascripts/src/lib/utils.ts rename to app/javascript/legacy_react/src/lib/utils.ts diff --git a/javascripts/src/lib/vjf_rules.ts b/app/javascript/legacy_react/src/lib/vjf_rules.ts similarity index 98% rename from javascripts/src/lib/vjf_rules.ts rename to app/javascript/legacy_react/src/lib/vjf_rules.ts index 26eef445..b9f2db7f 100644 --- a/javascripts/src/lib/vjf_rules.ts +++ b/app/javascript/legacy_react/src/lib/vjf_rules.ts @@ -1,7 +1,7 @@ // License: LGPL-3.0-or-later import * as Regex from './regex' import {Field, Form} from "mobx-react-form"; -import moment = require("moment"); +import moment from "moment"; interface ValidationInput { diff --git a/app/javascript/packs/create_new_offsite_payment_pane.js b/app/javascript/packs/create_new_offsite_payment_pane.js index f1bab5de..71a6323c 100644 --- a/app/javascript/packs/create_new_offsite_payment_pane.js +++ b/app/javascript/packs/create_new_offsite_payment_pane.js @@ -1,4 +1,4 @@ // License: LGPL-3.0-or-later // require a root component here. This will be treated as the root of a webpack package require('bootstrap-loader'); -require('../../../javascripts/app/create_new_offsite_payment_pane') \ No newline at end of file +require('../legacy_react/app/create_new_offsite_payment_pane') \ No newline at end of file diff --git a/app/javascript/packs/edit_payment_pane.js b/app/javascript/packs/edit_payment_pane.js index 3645948d..3b85237a 100644 --- a/app/javascript/packs/edit_payment_pane.js +++ b/app/javascript/packs/edit_payment_pane.js @@ -1,4 +1,4 @@ // License: LGPL-3.0-or-later // require a root component here. This will be treated as the root of a webpack package require('bootstrap-loader'); -require('../../../javascripts/app/edit_payment_pane') \ No newline at end of file +require('../legacy_react/app/edit_payment_pane') \ No newline at end of file diff --git a/app/javascript/packs/loading_indicator.ts b/app/javascript/packs/loading_indicator.ts index c4f2bfcf..86d287c4 100644 --- a/app/javascript/packs/loading_indicator.ts +++ b/app/javascript/packs/loading_indicator.ts @@ -1,5 +1,5 @@ // License: LGPL-3.0-or-later -require('../../../javascripts/app/loading_indicator') +require('../legacy_react/app/loading_indicator') diff --git a/app/javascript/packs/registration_page.js b/app/javascript/packs/registration_page.js index 59186633..7c93fb67 100644 --- a/app/javascript/packs/registration_page.js +++ b/app/javascript/packs/registration_page.js @@ -1,4 +1,4 @@ // License: LGPL-3.0-or-later require('bootstrap-loader'); -require('../../../javascripts/app/registration_page') +require('../legacy_react/app/registration_page') diff --git a/app/javascript/packs/session_login_page.js b/app/javascript/packs/session_login_page.js index 5d840db1..7e7147b2 100644 --- a/app/javascript/packs/session_login_page.js +++ b/app/javascript/packs/session_login_page.js @@ -1,3 +1,3 @@ // License: LGPL-3.0-or-later require('bootstrap-loader'); -require('../../../javascripts/app/session_login_page') \ No newline at end of file +require('../legacy_react/app/session_login_page') \ No newline at end of file diff --git a/babel.config.js b/babel.config.js index 79a42749..b585f6ff 100644 --- a/babel.config.js +++ b/babel.config.js @@ -18,56 +18,72 @@ module.exports = function(api) { return { presets: [ isTestEnv && [ - require('@babel/preset-env').default, + '@babel/preset-env', { targets: { - node: 'current' - } - } + node: 'current', + }, + modules: 'commonjs', + }, + '@babel/preset-react', ], (isProductionEnv || isDevelopmentEnv) && [ - require('@babel/preset-env').default, + '@babel/preset-env', { forceAllTransforms: true, useBuiltIns: 'entry', corejs: 3, modules: false, - exclude: ['transform-typeof-symbol'] - } + exclude: ['transform-typeof-symbol'], + }, ], - ['@babel/preset-typescript', { 'allExtensions': true, 'isTSX': true }] + [ + '@babel/preset-react', + { + development: isDevelopmentEnv || isTestEnv, + useBuiltIns: true, + }, + ], + ['@babel/preset-typescript', { 'allExtensions': true, 'isTSX': true }], ].filter(Boolean), plugins: [ - require('babel-plugin-macros'), - require('@babel/plugin-syntax-dynamic-import').default, - isTestEnv && require('babel-plugin-dynamic-import-node'), - require('@babel/plugin-transform-destructuring').default, + 'babel-plugin-macros', + '@babel/plugin-syntax-dynamic-import', + isTestEnv && 'babel-plugin-dynamic-import-node', + '@babel/plugin-transform-destructuring', + ["@babel/plugin-proposal-decorators", { legacy: true }], [ - require('@babel/plugin-proposal-class-properties').default, + '@babel/plugin-proposal-class-properties', { - loose: true - } + loose: true, + }, ], [ - require('@babel/plugin-proposal-object-rest-spread').default, + '@babel/plugin-proposal-object-rest-spread', { - useBuiltIns: true - } + useBuiltIns: true, + }, ], [ - require('@babel/plugin-transform-runtime').default, + '@babel/plugin-transform-runtime', { helpers: false, regenerator: true, - corejs: false - } + corejs: false, + }, ], [ - require('@babel/plugin-transform-regenerator').default, + '@babel/plugin-transform-regenerator', { - async: false - } - ] - ].filter(Boolean) + async: false, + }, + ], + isProductionEnv && [ + 'babel-plugin-transform-react-remove-prop-types', + { + removeImport: true, + }, + ], + ].filter(Boolean), } } diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 7ee10f4d..57bfeb6b 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -1,6 +1,16 @@ const { environment } = require('@rails/webpacker') +const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); +const path = require("path"); const erb = require('./loaders/erb') environment.loaders.prepend('erb', erb) - +environment.plugins.append( + "ForkTsCheckerWebpackPlugin", + new ForkTsCheckerWebpackPlugin({ + // this is a relative path to your project's TypeScript config + tsconfig: path.resolve(__dirname, "../../tsconfig.json"), + // non-async so type checking will block compilation + async: false, + }) + ); module.exports = environment diff --git a/config/webpacker.yml b/config/webpacker.yml index 278d979f..56fed7d4 100644 --- a/config/webpacker.yml +++ b/config/webpacker.yml @@ -6,8 +6,7 @@ default: &default public_root_path: public public_output_path: packs cache_path: tmp/cache/webpacker - check_yarn_integrity: false - webpack_compile_output: false + webpack_compile_output: true # Additional paths webpack should lookup modules # ['app/assets', 'engine/foo/app/assets'] @@ -37,6 +36,7 @@ default: &default - .erb - .tsx - .ts + - .jsx - .mjs - .js - .sass @@ -55,9 +55,6 @@ development: <<: *default compile: true - # Verifies that correct packages and versions are installed by inspecting package.json, yarn.lock, and node_modules - check_yarn_integrity: false - # Reference: https://webpack.js.org/configuration/dev-server/ dev_server: https: false @@ -72,6 +69,7 @@ development: disable_host_check: true use_local_ip: false quiet: false + pretty: false headers: 'Access-Control-Allow-Origin': '*' watch_options: diff --git a/package.json b/package.json index 7a0abbd1..e7461297 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,12 @@ "jest": "jest" }, "devDependencies": { + "@babel/core": "^7.0.0", + "@babel/plugin-proposal-decorators": "^7.10.0", "@babel/preset-env": "^7.7.1", + "@babel/preset-react": "^7.10.0", + "@babel/preset-typescript": "^7.9.0", + "@rails/webpacker": "^5.1.1", "@types/activestorage": "^5.2.2", "@types/color": "^3.0.0", "@types/enzyme": "^3.1.9", @@ -25,14 +30,15 @@ "@types/lodash": "^4.14.106", "@types/moment-timezone": "^0.5.9", "@types/prop-types": "^15.5.5", - "@types/react": "^16.9.11", - "@types/react-dom": "^16.9.4", + "@types/react": "^16.9.35", + "@types/react-dom": "^16.9.8", "@types/react-intl": "^2.3.7", "@types/react-test-renderer": "^16.0.1", "@types/react-text-mask": "^5.4.2", "@types/react-transition-group": "^2.9.0", "@types/sinon": "^4.3.3", "@types/validator": "^9.4.1", + "babel-plugin-transform-react-remove-prop-types": "^0.4.24", "bootstrap": "^3.4.1", "bootstrap-loader": "github:houdiniproject/bootstrap-loader#compiled_namespaced_3", "bootstrap-sass": "^3.3.7", @@ -40,6 +46,7 @@ "enzyme": "^3.8.0", "enzyme-adapter-react-16": "^1.9.1", "enzyme-to-json": "^3.3.3", + "fork-ts-checker-webpack-plugin": "^4.1.6", "jest": "^24.1.0", "jest-enzyme": "^7.0.1", "jsdom": "^11.10.0", @@ -50,17 +57,14 @@ "sinon": "^5.0.7", "style-loader": "^0.21.0", "ts-jest": "^24.0.0", - "typescript": "^3.7.2", + "typescript": "^3.9.3", "url-loader": "^1.0.1", "webpack": "^4.0.0", "webpack-cli": "^3.3.11", "webpack-dev-server": "^3.11.0" }, "dependencies": { - "@babel/core": "^7.0.0", - "@babel/preset-typescript": "^7.9.0", "@rails/activestorage": "^6.0.2-2", - "@rails/webpacker": "~5.1.1", "attr-binder": "0.3.1", "aws-sdk": "^2.402.0", "chart.js": "2.1.4", diff --git a/tsconfig.json b/tsconfig.json index 99893565..16b0ff12 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,6 +13,8 @@ "paths": { "*": [ "./types/*"], "@rails/activestorage": ["./types/rails__activestorage"] }, "typeRoots" : [ "node_modules/@types", - "./types"] + "./types"], + "allowSyntheticDefaultImports": true, + "esModuleInterop":true } } \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 8c8d953c..db28bdf1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,35 +2,35 @@ # yarn lockfile v1 -"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": +"@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== dependencies: "@babel/highlight" "^7.8.3" -"@babel/compat-data@^7.8.6", "@babel/compat-data@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.9.0.tgz#04815556fc90b0c174abd2c0c1bb966faa036a6c" - integrity sha512-zeFQrr+284Ekvd9e7KAX954LkapWiOmQtsfHirhxqfdlX6MEC32iRE+pqUGlYIBchdevaCwvzxWGSy/YBNI85g== +"@babel/compat-data@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.10.0.tgz#1e9129ec36bc7cc5ec202801d8af9529699b8d5e" + integrity sha512-H59nKm/7ATMfocMobbSk4PkeAerKqoxk+EYBT0kV5sol0e8GBpGNHseZNNYX0VOItKngIf6GgUpEOAlOLIUvDA== dependencies: - browserslist "^4.9.1" + browserslist "^4.12.0" invariant "^2.2.4" semver "^5.5.0" "@babel/core@^7.0.0", "@babel/core@^7.1.0", "@babel/core@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.9.0.tgz#ac977b538b77e132ff706f3b8a4dbad09c03c56e" - integrity sha512-kWc7L0fw1xwvI0zi8OKVBuxRVefwGOrKSQMvrQ3dW+bIIavBY3/NpXmpjMy7bQnLgwgzWQZ8TlM57YHpHNHz4w== + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.10.0.tgz#a6fe5db77ebfb61e0da6c5c36aaf14aab07b2b44" + integrity sha512-FGgV2XyPoVtYDvbFXlukEWt13Afka4mBRQ2CoTsHxpgVGO6XfgtT6eI+WyjQRGGTL90IDkIVmme8riFCLZ8lUw== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.0" + "@babel/generator" "^7.10.0" "@babel/helper-module-transforms" "^7.9.0" - "@babel/helpers" "^7.9.0" - "@babel/parser" "^7.9.0" - "@babel/template" "^7.8.6" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/helpers" "^7.10.0" + "@babel/parser" "^7.10.0" + "@babel/template" "^7.10.0" + "@babel/traverse" "^7.10.0" + "@babel/types" "^7.10.0" convert-source-map "^1.7.0" debug "^4.1.0" gensync "^1.0.0-beta.1" @@ -40,7 +40,7 @@ semver "^5.4.1" source-map "^0.5.0" -"@babel/generator@^7.10.0": +"@babel/generator@^7.10.0", "@babel/generator@^7.4.0": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.10.0.tgz#a238837896edf35ee5fbfb074548d3256b4bc55d" integrity sha512-ThoWCJHlgukbtCP79nAK4oLqZt5fVo70AHUni/y8Jotyg5rtJiG2FVl+iJjRNKIyl4hppqztLyAoEWcCvqyOFQ== @@ -50,16 +50,6 @@ lodash "^4.17.13" source-map "^0.5.0" -"@babel/generator@^7.4.0", "@babel/generator@^7.9.0", "@babel/generator@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.9.5.tgz#27f0917741acc41e6eaaced6d68f96c3fa9afaf9" - integrity sha512-GbNIxVB3ZJe3tLeDm1HSn2AhuD/mVcyLDpgtLXa5tplmWrJdF/elxB56XNqCuD6szyNkDi6wuoKXln3QeBmCHQ== - dependencies: - "@babel/types" "^7.9.5" - jsesc "^2.5.1" - lodash "^4.17.13" - source-map "^0.5.0" - "@babel/helper-annotate-as-pure@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.8.3.tgz#60bc0bc657f63a0924ff9a4b4a0b24a13cf4deee" @@ -75,18 +65,35 @@ "@babel/helper-explode-assignable-expression" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-compilation-targets@^7.8.7": - version "7.8.7" - resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.8.7.tgz#dac1eea159c0e4bd46e309b5a1b04a66b53c1dde" - integrity sha512-4mWm8DCK2LugIS+p1yArqvG1Pf162upsIsjE7cNBjez+NjliQpVhj20obE520nao0o14DaTnFJv+Fw5a0JpoUw== +"@babel/helper-builder-react-jsx-experimental@^7.9.0": + version "7.9.5" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx-experimental/-/helper-builder-react-jsx-experimental-7.9.5.tgz#0b4b3e04e6123f03b404ca4dfd6528fe6bb92fe3" + integrity sha512-HAagjAC93tk748jcXpZ7oYRZH485RCq/+yEv9SIWezHRPv9moZArTnkUNciUNzvwHUABmiWKlcxJvMcu59UwTg== dependencies: - "@babel/compat-data" "^7.8.6" - browserslist "^4.9.1" + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-module-imports" "^7.8.3" + "@babel/types" "^7.9.5" + +"@babel/helper-builder-react-jsx@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/helper-builder-react-jsx/-/helper-builder-react-jsx-7.9.0.tgz#16bf391990b57732700a3278d4d9a81231ea8d32" + integrity sha512-weiIo4gaoGgnhff54GQ3P5wsUQmnSwpkvU0r6ZHq6TzoSzKy4JxHEgnxNytaKbov2a9z/CVNyzliuCOUPEX3Jw== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/types" "^7.9.0" + +"@babel/helper-compilation-targets@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.10.0.tgz#c2734604ddfaa616479759a0cc2593d1928304bd" + integrity sha512-PW5Hlc1cQ8bLzY7YsLJP6PQ7GR6ZD8Av4JlP3DZk6QaZJvptsXNDn4Su64EjKAetLTJhVPDp8AEC+j2O6b/Gpg== + dependencies: + "@babel/compat-data" "^7.10.0" + browserslist "^4.12.0" invariant "^2.2.4" levenary "^1.1.1" semver "^5.5.0" -"@babel/helper-create-class-features-plugin@^7.10.0": +"@babel/helper-create-class-features-plugin@^7.10.0", "@babel/helper-create-class-features-plugin@^7.8.3": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.10.0.tgz#3a2b7b86f6365ea4ac3837a49ec5791e65217944" integrity sha512-n4tPJaI0iuLRayriXTQ8brP3fMA/fNmxpxswfNuhe4qXQbcCWzeAqm6SeR/KExIOcdCvOh/KkPQVgBsjcb0oqA== @@ -98,18 +105,6 @@ "@babel/helper-replace-supers" "^7.10.0" "@babel/helper-split-export-declaration" "^7.8.3" -"@babel/helper-create-class-features-plugin@^7.8.3": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.9.5.tgz#79753d44017806b481017f24b02fd4113c7106ea" - integrity sha512-IipaxGaQmW4TfWoXdqjY0TzoXQ1HRS0kPpEgvjosb3u7Uedcq297xFqDQiCcQtRRwzIMif+N1MLVI8C5a4/PAA== - dependencies: - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/helper-plugin-utils" "^7.8.3" - "@babel/helper-replace-supers" "^7.8.6" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/helper-create-regexp-features-plugin@^7.8.3", "@babel/helper-create-regexp-features-plugin@^7.8.8": version "7.8.8" resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.8.8.tgz#5d84180b588f560b7864efaeea89243e58312087" @@ -166,13 +161,6 @@ dependencies: "@babel/types" "^7.10.0" -"@babel/helper-member-expression-to-functions@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.8.3.tgz#659b710498ea6c1d9907e0c73f206eee7dadc24c" - integrity sha512-fO4Egq88utkQFjbPrSHGmGLFqmrshs11d46WI+WZDESt7Wu7wN2G2Iu+NMMZJFDOVRHAMIkB5SNh30NtwCA7RA== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-module-imports@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.8.3.tgz#7fe39589b39c016331b6b8c3f441e8f0b1419498" @@ -193,20 +181,13 @@ "@babel/types" "^7.9.0" lodash "^4.17.13" -"@babel/helper-optimise-call-expression@^7.10.0": +"@babel/helper-optimise-call-expression@^7.10.0", "@babel/helper-optimise-call-expression@^7.8.3": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.10.0.tgz#6dcfb565842f43bed31b24f3e4277f18826e5e79" integrity sha512-HgMd8QKA8wMJs5uK/DYKdyzJAEuGt1zyDp9wLMlMR6LitTQTHPUE+msC82ZsEDwq+U3/yHcIXIngRm9MS4IcIg== dependencies: "@babel/types" "^7.10.0" -"@babel/helper-optimise-call-expression@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.8.3.tgz#7ed071813d09c75298ef4f208956006b6111ecb9" - integrity sha512-Kag20n86cbO2AvHca6EJsvqAd82gc6VMGule4HwebwMlwkpXuVqrNRj6CkCV2sKxgi9MyAUnZVnZ6lJ1/vKhHQ== - dependencies: - "@babel/types" "^7.8.3" - "@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.8.0", "@babel/helper-plugin-utils@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.8.3.tgz#9ea293be19babc0f52ff8ca88b34c3611b208670" @@ -230,7 +211,7 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helper-replace-supers@^7.10.0": +"@babel/helper-replace-supers@^7.10.0", "@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.10.0.tgz#26bc22ee1a35450934d2e2a9b27de10a22fac9d6" integrity sha512-erl4iVeiANf14JszXP7b69bSrz3e3+qW09pVvEmTWwzRQEOoyb1WFlYCA8d/VjVZGYW8+nGpLh7swf9CifH5wg== @@ -240,16 +221,6 @@ "@babel/traverse" "^7.10.0" "@babel/types" "^7.10.0" -"@babel/helper-replace-supers@^7.8.3", "@babel/helper-replace-supers@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.8.6.tgz#5ada744fd5ad73203bf1d67459a27dcba67effc8" - integrity sha512-PeMArdA4Sv/Wf4zXwBKPqVj7n9UF/xg6slNRtZW84FM7JpE1CbG8B612FyM4cxrf4fMAMGO0kR7voy1ForHHFA== - dependencies: - "@babel/helper-member-expression-to-functions" "^7.8.3" - "@babel/helper-optimise-call-expression" "^7.8.3" - "@babel/traverse" "^7.8.6" - "@babel/types" "^7.8.6" - "@babel/helper-simple-access@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/helper-simple-access/-/helper-simple-access-7.8.3.tgz#7f8109928b4dab4654076986af575231deb639ae" @@ -280,14 +251,14 @@ "@babel/traverse" "^7.8.3" "@babel/types" "^7.8.3" -"@babel/helpers@^7.9.0": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.9.2.tgz#b42a81a811f1e7313b88cba8adc66b3d9ae6c09f" - integrity sha512-JwLvzlXVPjO8eU9c/wF9/zOIN7X6h8DYf7mG4CiFRZRvZNKEF5dQ3H3V+ASkHoIB3mWhatgl5ONhyqHRI6MppA== +"@babel/helpers@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.10.0.tgz#634400a0977b8dcf7b311761a77ca94ed974b3b6" + integrity sha512-lQtFJoDZAGf/t2PgR6Z59Q2MwjvOGGsxZ0BAlsrgyDhKuMbe63EfbQmVmcLfyTBj8J4UtiadQimcotvYVg/kVQ== dependencies: - "@babel/template" "^7.8.3" - "@babel/traverse" "^7.9.0" - "@babel/types" "^7.9.0" + "@babel/template" "^7.10.0" + "@babel/traverse" "^7.10.0" + "@babel/types" "^7.10.0" "@babel/highlight@^7.8.3": version "7.9.0" @@ -298,12 +269,7 @@ chalk "^2.0.0" js-tokens "^4.0.0" -"@babel/parser@^7.1.0", "@babel/parser@^7.4.3", "@babel/parser@^7.8.6", "@babel/parser@^7.9.0": - version "7.9.4" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.9.4.tgz#68a35e6b0319bbc014465be43828300113f2f2e8" - integrity sha512-bC49otXX6N0/VYhgOMh4gnP26E9xnDZK3TmbNpxYzzz9BQLBosQwfyOe9/cXUU3txYhTzLCbcqd5c8y/OmCjHA== - -"@babel/parser@^7.10.0": +"@babel/parser@^7.1.0", "@babel/parser@^7.10.0", "@babel/parser@^7.4.3": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.10.0.tgz#8eca3e71a73dd562c5222376b08253436bb4995b" integrity sha512-fnDUl1Uy2gThM4IFVW4ISNHqr3cJrCsRkSCasFgx0XDO9JcttDS5ytyBc4Cu4X1+fjoo3IVvFbRD6TeFlHJlEQ== @@ -325,6 +291,15 @@ "@babel/helper-create-class-features-plugin" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-proposal-decorators@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-decorators/-/plugin-proposal-decorators-7.10.0.tgz#aa1c82288d9af1f2a5bc759e5dffbca8f8d01ea1" + integrity sha512-PTlxQfx0fZjOYlLe+gAhpb6Lph3zr03lpzqnzI8bWtcxDo/98rhO2adxe87F7OHg1G65nXxQ9ChPvB/0A3qSAg== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.10.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-decorators" "^7.8.3" + "@babel/plugin-proposal-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.8.3.tgz#38c4fe555744826e97e2ae930b0fb4cc07e66054" @@ -333,10 +308,10 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-dynamic-import" "^7.8.0" -"@babel/plugin-proposal-json-strings@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.8.3.tgz#da5216b238a98b58a1e05d6852104b10f9a70d6b" - integrity sha512-KGhQNZ3TVCQG/MjRbAUwuH+14y9q0tpxs1nWWs3pbSleRdDro9SAMMDyye8HhY1gqZ7/NqIc8SKhya0wRDgP1Q== +"@babel/plugin-proposal-json-strings@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.10.0.tgz#72926c31c14ff4f04916a0b17d376cdfb7fa1d84" + integrity sha512-n4oQLAAXTFj0OusjIbr6bcvVQf8oH6QziwAK8QNtKhjJAg71+hnU2rZDZYkYMmfOZ46dCWf+ybbHJ7hxfrzFlw== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-json-strings" "^7.8.0" @@ -357,10 +332,10 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-numeric-separator" "^7.8.3" -"@babel/plugin-proposal-object-rest-spread@^7.9.0", "@babel/plugin-proposal-object-rest-spread@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.9.5.tgz#3fd65911306d8746014ec0d0cf78f0e39a149116" - integrity sha512-VP2oXvAf7KCYTthbUHwBlewbl1Iq059f6seJGsxMizaCdgHIeczOr7FBqELhSqfkIl04Fi8okzWzl63UKbQmmg== +"@babel/plugin-proposal-object-rest-spread@^7.10.0", "@babel/plugin-proposal-object-rest-spread@^7.9.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.10.0.tgz#d27b0910b637f7c9d9a5629f2adcd04dc9ea4e69" + integrity sha512-DOD+4TqMcRKJdAfN08+v9cciK5d0HW5hwTndOoKZEfEzU/mRrKboheD5mnWU4Q96VOnDdAj86kKjZhoQyG6s+A== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-object-rest-spread" "^7.8.0" @@ -374,14 +349,22 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.0" -"@babel/plugin-proposal-optional-chaining@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.9.0.tgz#31db16b154c39d6b8a645292472b98394c292a58" - integrity sha512-NDn5tu3tcv4W30jNhmc2hyD5c56G6cXx4TesJubhxrJeCvuuMpttxr0OnNCqbZGhFjLrg+NIhxxC+BK5F6yS3w== +"@babel/plugin-proposal-optional-chaining@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.10.0.tgz#f9bdcd5cbf2e3037674903a45e56ed0cbaea1550" + integrity sha512-bn+9XT8Y6FJCO37ewj4E1gIirR35nDm+mGcqQV4dM3LKSVp3QTAU3f65Z0ld4y6jdfAlv2VKzCh4mezhRnl+6Q== dependencies: "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.0" +"@babel/plugin-proposal-private-methods@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.8.3.tgz#01248c6c8dc292116b3b4ebd746150f4f0728bab" + integrity sha512-ysLAper960yy1TVXa2lMYdCQIGqtUXo8sVb+zYE7UTiZSLs6/wbZ0PrrXEKESJcK3SgFWrF8WpsaDzdslhuoZA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-proposal-unicode-property-regex@^7.4.4", "@babel/plugin-proposal-unicode-property-regex@^7.8.3": version "7.8.8" resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.8.8.tgz#ee3a95e90cdc04fe8cd92ec3279fa017d68a0d1d" @@ -397,6 +380,20 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-class-properties@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.8.3.tgz#6cb933a8872c8d359bfde69bbeaae5162fd1e8f7" + integrity sha512-UcAyQWg2bAN647Q+O811tG9MrJ38Z10jjhQdKNAL8fsyPzE3cCN/uT+f55cFVY4aGO4jqJAvmqsuY3GQDwAoXg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-syntax-decorators@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-decorators/-/plugin-syntax-decorators-7.8.3.tgz#8d2c15a9f1af624b0025f961682a9d53d3001bda" + integrity sha512-8Hg4dNNT9/LcA1zQlfwuKR8BUc/if7Q7NkTam9sGTcJphLwpf2g4S42uhspQrIrR+dpzE0dtTqBVFoHl8GtnnQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-dynamic-import@^7.8.0", "@babel/plugin-syntax-dynamic-import@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz#62bf98b2da3cd21d626154fc96ee5b3cb68eacb3" @@ -411,6 +408,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.0" +"@babel/plugin-syntax-jsx@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.8.3.tgz#521b06c83c40480f1e58b4fd33b92eceb1d6ea94" + integrity sha512-WxdW9xyLgBdefoo0Ynn3MRSkhe5tFVxxKNVdnZSh318WrG2e2jH+E9wd/++JsqcLJZPfz87njQJ8j2Upjm0M0A== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-nullish-coalescing-operator@^7.8.0": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz#167ed70368886081f74b5c36c65a88c03b66d1a9" @@ -483,10 +487,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-block-scoping@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.8.3.tgz#97d35dab66857a437c166358b91d09050c868f3a" - integrity sha512-pGnYfm7RNRgYRi7bids5bHluENHqJhrV4bCZRwc5GamaWIIs07N4rZECcmJL6ZClwjDz1GbdMZFtPs27hTB06w== +"@babel/plugin-transform-block-scoping@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.10.0.tgz#5d7aa0cf921ec91bdc97c9b311bf1fce0ea979b0" + integrity sha512-AoMn0D3nLG9i71useuBrZZTnHbjnhcaTXCckUtOx3JPuhGGJdOUYMwOV9niPJ+nZCk52dfLLqbmV3pBMCRQLNw== dependencies: "@babel/helper-plugin-utils" "^7.8.3" lodash "^4.17.13" @@ -512,10 +516,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-destructuring@^7.8.8", "@babel/plugin-transform-destructuring@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.9.5.tgz#72c97cf5f38604aea3abf3b935b0e17b1db76a50" - integrity sha512-j3OEsGel8nHL/iusv/mRd5fYZ3DrOxWC82x0ogmdN/vHfAP4MYw+AFKYanzWlktNwikKvlzUV//afBW5FTp17Q== +"@babel/plugin-transform-destructuring@^7.10.0", "@babel/plugin-transform-destructuring@^7.8.8": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.10.0.tgz#59145194029721e49e511afb4bdd1d2f38369180" + integrity sha512-yKoghHpYbC0eM+6o6arPUJT9BQBvOOn8iOCEHwFvkJ5gjAxYmoUaAuLwaoA9h2YvC6dzcRI0KPQOpRXr8qQTxQ== dependencies: "@babel/helper-plugin-utils" "^7.8.3" @@ -542,10 +546,10 @@ "@babel/helper-builder-binary-assignment-operator-visitor" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-for-of@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.9.0.tgz#0f260e27d3e29cd1bb3128da5e76c761aa6c108e" - integrity sha512-lTAnWOpMwOXpyDx06N+ywmF3jNbafZEqZ96CGYabxHrxNX8l5ny7dt4bK/rGwAh9utyP2b2Hv7PlZh1AAS54FQ== +"@babel/plugin-transform-for-of@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.10.0.tgz#ff2bf95dc1deb9b309c7fd78d9620ac9266a3efe" + integrity sha512-0ldl5xEe9kbuhB1cDqs17JiBPEm1+6/LH7loo29+MAJOyB/xbpLI/u6mRzDPjr0nYL7z0S14FPT4hs2gH8Im9Q== dependencies: "@babel/helper-plugin-utils" "^7.8.3" @@ -571,34 +575,34 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-modules-amd@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.0.tgz#19755ee721912cf5bb04c07d50280af3484efef4" - integrity sha512-vZgDDF003B14O8zJy0XXLnPH4sg+9X5hFBBGN1V+B2rgrB+J2xIypSN6Rk9imB2hSTHQi5OHLrFWsZab1GMk+Q== +"@babel/plugin-transform-modules-amd@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.9.6.tgz#8539ec42c153d12ea3836e0e3ac30d5aae7b258e" + integrity sha512-zoT0kgC3EixAyIAU+9vfaUVKTv9IxBDSabgHoUCBP6FqEJ+iNiN7ip7NBKcYqbfUDfuC2mFCbM7vbu4qJgOnDw== dependencies: "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-commonjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.0.tgz#e3e72f4cbc9b4a260e30be0ea59bdf5a39748940" - integrity sha512-qzlCrLnKqio4SlgJ6FMMLBe4bySNis8DFn1VkGmOcxG9gqEyPIOzeQrA//u0HAKrWpJlpZbZMPB1n/OPa4+n8g== +"@babel/plugin-transform-modules-commonjs@^7.9.6": + version "7.9.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.9.6.tgz#64b7474a4279ee588cacd1906695ca721687c277" + integrity sha512-7H25fSlLcn+iYimmsNe3uK1at79IE6SKW9q0/QeEHTMC9MdOZ+4bA+T1VFB5fgOqBWoqlifXRzYD0JPdmIrgSQ== dependencies: "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" "@babel/helper-simple-access" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" -"@babel/plugin-transform-modules-systemjs@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.9.0.tgz#e9fd46a296fc91e009b64e07ddaa86d6f0edeb90" - integrity sha512-FsiAv/nao/ud2ZWy4wFacoLOm5uxl0ExSQ7ErvP7jpoihLR6Cq90ilOFyX9UXct3rbtKsAiZ9kFt5XGfPe/5SQ== +"@babel/plugin-transform-modules-systemjs@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.10.0.tgz#815aa9b9d59224ed1bb5d4cbb3c86c4d7e12d9bf" + integrity sha512-L/1xADoyJeb01fqKiHhl4ghAJOnFcHvx2JQA7bc8zdaDFDU4k62CJmXqDtNtJUNiOwlHZLWg1l7/Twf1aWARQw== dependencies: "@babel/helper-hoist-variables" "^7.8.3" "@babel/helper-module-transforms" "^7.9.0" "@babel/helper-plugin-utils" "^7.8.3" - babel-plugin-dynamic-import-node "^2.3.0" + babel-plugin-dynamic-import-node "^2.3.3" "@babel/plugin-transform-modules-umd@^7.9.0": version "7.9.0" @@ -645,6 +649,56 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" +"@babel/plugin-transform-react-display-name@^7.8.3": + version "7.8.3" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.8.3.tgz#70ded987c91609f78353dd76d2fb2a0bb991e8e5" + integrity sha512-3Jy/PCw8Fe6uBKtEgz3M82ljt+lTg+xJaM4og+eyu83qLT87ZUSckn0wy7r31jflURWLO83TW6Ylf7lyXj3m5A== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + +"@babel/plugin-transform-react-jsx-development@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.9.0.tgz#3c2a130727caf00c2a293f0aed24520825dbf754" + integrity sha512-tK8hWKrQncVvrhvtOiPpKrQjfNX3DtkNLSX4ObuGcpS9p0QrGetKmlySIGR07y48Zft8WVgPakqd/bk46JrMSw== + dependencies: + "@babel/helper-builder-react-jsx-experimental" "^7.9.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" + +"@babel/plugin-transform-react-jsx-self@^7.9.0": + version "7.9.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.9.0.tgz#f4f26a325820205239bb915bad8e06fcadabb49b" + integrity sha512-K2ObbWPKT7KUTAoyjCsFilOkEgMvFG+y0FqOl6Lezd0/13kMkkjHskVsZvblRPj1PHA44PrToaZANrryppzTvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" + +"@babel/plugin-transform-react-jsx-source@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.10.0.tgz#0e24978505130a79bb8ee1af15a1a7d8e783347d" + integrity sha512-EmUZ2YYXK6YFIdSxUJ1thg7gIBMHSEp8nGS6GwkXGpGdplpmOhj6azYjszT8YcFt6HyPElycDOd2lXckzN+OEw== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" + +"@babel/plugin-transform-react-jsx@^7.9.4": + version "7.9.4" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.9.4.tgz#86f576c8540bd06d0e95e0b61ea76d55f6cbd03f" + integrity sha512-Mjqf3pZBNLt854CK0C/kRuXAnE6H/bo7xYojP+WGtX8glDGSibcwnsWwhwoSuRg0+EBnxPC1ouVnuetUIlPSAw== + dependencies: + "@babel/helper-builder-react-jsx" "^7.9.0" + "@babel/helper-builder-react-jsx-experimental" "^7.9.0" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-syntax-jsx" "^7.8.3" + +"@babel/plugin-transform-react-pure-annotations@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.10.0.tgz#b3794b3c28e6289f104a6fc49ddf6e71401a84cf" + integrity sha512-rn8QOUd0wnJYKGROZ9GYIPBbl/c3aC2tuMh1dmkklrL0l3D5MzXmQBlrsWTizbFnJC16TkEHwSs+a0rEO/hvcQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.8.3" + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-regenerator@^7.8.7": version "7.8.7" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.8.7.tgz#5e46a0dca2bee1ad8285eb0527e6abc9c37672f8" @@ -660,9 +714,9 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-transform-runtime@^7.9.0": - version "7.9.0" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.9.0.tgz#45468c0ae74cc13204e1d3b1f4ce6ee83258af0b" - integrity sha512-pUu9VSf3kI1OqbWINQ7MaugnitRss1z533436waNXp+0N3ur3zfut37sXiQMxkuCF4VUjwZucen/quskCh7NHw== + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.10.0.tgz#16e50ba682aa9925b94123a622d996cadd4cbef7" + integrity sha512-SWIc5IJnoLHk9qVRvvpebUW5lafStcKlLcqELMiNOApVIxPbCtkQfLRMCdaEKw4X92JItFKdoBxv2udiyGwFtg== dependencies: "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" @@ -676,10 +730,10 @@ dependencies: "@babel/helper-plugin-utils" "^7.8.3" -"@babel/plugin-transform-spread@^7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.8.3.tgz#9c8ffe8170fdfb88b114ecb920b82fb6e95fe5e8" - integrity sha512-CkuTU9mbmAoFOI1tklFWYYbzX5qCIZVXPVy0jpXgGwkplCndQAa58s2jr66fTeQnA64bDox0HL4U56CFYoyC7g== +"@babel/plugin-transform-spread@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.10.0.tgz#6918d9b2b52c604802bd50a5f22b649efddf9af6" + integrity sha512-P3Zj04ylqumJBjmjylNl05ZHRo4j4gFNG7P70loys0//q5BTe30E8xIj6PnqEWAfsPYu2sdIPcJeeQdclqlM6A== dependencies: "@babel/helper-plugin-utils" "^7.8.3" @@ -715,6 +769,13 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-syntax-typescript" "^7.8.3" +"@babel/plugin-transform-unicode-escapes@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.10.0.tgz#63b4da633af14740b6570b928a2d5537495314cb" + integrity sha512-6DwSPQzJ9kSRI1kNFfVAeYdeH7sUH0c1NOYSBGnpJ1ZUZ7mxPY1hxeAqzcrO5NKlOx7ghcy4nAbfFWTPx5IVEg== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-unicode-regex@^7.8.3": version "7.8.3" resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.8.3.tgz#0cef36e3ba73e5c57273effb182f46b91a1ecaad" @@ -724,24 +785,27 @@ "@babel/helper-plugin-utils" "^7.8.3" "@babel/preset-env@^7.7.1", "@babel/preset-env@^7.9.0": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.9.5.tgz#8ddc76039bc45b774b19e2fc548f6807d8a8919f" - integrity sha512-eWGYeADTlPJH+wq1F0wNfPbVS1w1wtmMJiYk55Td5Yu28AsdR9AsC97sZ0Qq8fHqQuslVSIYSGJMcblr345GfQ== + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.10.0.tgz#2b1d9c0cf41fdf68f64d8183a567a14f70861f99" + integrity sha512-UOZNyiZRvIGvIudjCB8Y8OVkpAvlslec4qgwC73yEvx3Puz0c/xc28Yru36y5K+StOkPPM+VldTsmXPht5LpSg== dependencies: - "@babel/compat-data" "^7.9.0" - "@babel/helper-compilation-targets" "^7.8.7" + "@babel/compat-data" "^7.10.0" + "@babel/helper-compilation-targets" "^7.10.0" "@babel/helper-module-imports" "^7.8.3" "@babel/helper-plugin-utils" "^7.8.3" "@babel/plugin-proposal-async-generator-functions" "^7.8.3" + "@babel/plugin-proposal-class-properties" "^7.8.3" "@babel/plugin-proposal-dynamic-import" "^7.8.3" - "@babel/plugin-proposal-json-strings" "^7.8.3" + "@babel/plugin-proposal-json-strings" "^7.10.0" "@babel/plugin-proposal-nullish-coalescing-operator" "^7.8.3" "@babel/plugin-proposal-numeric-separator" "^7.8.3" - "@babel/plugin-proposal-object-rest-spread" "^7.9.5" + "@babel/plugin-proposal-object-rest-spread" "^7.10.0" "@babel/plugin-proposal-optional-catch-binding" "^7.8.3" - "@babel/plugin-proposal-optional-chaining" "^7.9.0" + "@babel/plugin-proposal-optional-chaining" "^7.10.0" + "@babel/plugin-proposal-private-methods" "^7.8.3" "@babel/plugin-proposal-unicode-property-regex" "^7.8.3" "@babel/plugin-syntax-async-generators" "^7.8.0" + "@babel/plugin-syntax-class-properties" "^7.8.3" "@babel/plugin-syntax-dynamic-import" "^7.8.0" "@babel/plugin-syntax-json-strings" "^7.8.0" "@babel/plugin-syntax-nullish-coalescing-operator" "^7.8.0" @@ -753,20 +817,20 @@ "@babel/plugin-transform-arrow-functions" "^7.8.3" "@babel/plugin-transform-async-to-generator" "^7.8.3" "@babel/plugin-transform-block-scoped-functions" "^7.8.3" - "@babel/plugin-transform-block-scoping" "^7.8.3" + "@babel/plugin-transform-block-scoping" "^7.10.0" "@babel/plugin-transform-classes" "^7.9.5" "@babel/plugin-transform-computed-properties" "^7.8.3" - "@babel/plugin-transform-destructuring" "^7.9.5" + "@babel/plugin-transform-destructuring" "^7.10.0" "@babel/plugin-transform-dotall-regex" "^7.8.3" "@babel/plugin-transform-duplicate-keys" "^7.8.3" "@babel/plugin-transform-exponentiation-operator" "^7.8.3" - "@babel/plugin-transform-for-of" "^7.9.0" + "@babel/plugin-transform-for-of" "^7.10.0" "@babel/plugin-transform-function-name" "^7.8.3" "@babel/plugin-transform-literals" "^7.8.3" "@babel/plugin-transform-member-expression-literals" "^7.8.3" - "@babel/plugin-transform-modules-amd" "^7.9.0" - "@babel/plugin-transform-modules-commonjs" "^7.9.0" - "@babel/plugin-transform-modules-systemjs" "^7.9.0" + "@babel/plugin-transform-modules-amd" "^7.9.6" + "@babel/plugin-transform-modules-commonjs" "^7.9.6" + "@babel/plugin-transform-modules-systemjs" "^7.10.0" "@babel/plugin-transform-modules-umd" "^7.9.0" "@babel/plugin-transform-named-capturing-groups-regex" "^7.8.3" "@babel/plugin-transform-new-target" "^7.8.3" @@ -776,14 +840,15 @@ "@babel/plugin-transform-regenerator" "^7.8.7" "@babel/plugin-transform-reserved-words" "^7.8.3" "@babel/plugin-transform-shorthand-properties" "^7.8.3" - "@babel/plugin-transform-spread" "^7.8.3" + "@babel/plugin-transform-spread" "^7.10.0" "@babel/plugin-transform-sticky-regex" "^7.8.3" "@babel/plugin-transform-template-literals" "^7.8.3" "@babel/plugin-transform-typeof-symbol" "^7.8.4" + "@babel/plugin-transform-unicode-escapes" "^7.10.0" "@babel/plugin-transform-unicode-regex" "^7.8.3" "@babel/preset-modules" "^0.1.3" - "@babel/types" "^7.9.5" - browserslist "^4.9.1" + "@babel/types" "^7.10.0" + browserslist "^4.12.0" core-js-compat "^3.6.2" invariant "^2.2.2" levenary "^1.1.1" @@ -800,6 +865,19 @@ "@babel/types" "^7.4.4" esutils "^2.0.2" +"@babel/preset-react@^7.10.0": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/preset-react/-/preset-react-7.10.0.tgz#aa737c937d982037744c73180353c5c639b9ca2f" + integrity sha512-3bHAfSRGTciFb1c7qlPCeGiL1TErUANc5AmjXE5+9/l6ePyLoCvHPxqdk94PUGwTn6/VOZSDDWtkC1cYsaUUkA== + dependencies: + "@babel/helper-plugin-utils" "^7.8.3" + "@babel/plugin-transform-react-display-name" "^7.8.3" + "@babel/plugin-transform-react-jsx" "^7.9.4" + "@babel/plugin-transform-react-jsx-development" "^7.9.0" + "@babel/plugin-transform-react-jsx-self" "^7.9.0" + "@babel/plugin-transform-react-jsx-source" "^7.10.0" + "@babel/plugin-transform-react-pure-annotations" "^7.10.0" + "@babel/preset-typescript@^7.9.0": version "7.9.0" resolved "https://registry.yarnpkg.com/@babel/preset-typescript/-/preset-typescript-7.9.0.tgz#87705a72b1f0d59df21c179f7c3d2ef4b16ce192" @@ -809,37 +887,22 @@ "@babel/plugin-transform-typescript" "^7.9.0" "@babel/runtime@^7.1.2", "@babel/runtime@^7.7.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.9.2": - version "7.9.2" - resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.9.2.tgz#d90df0583a3a252f09aaa619665367bae518db06" - integrity sha512-NE2DtOdufG7R5vnfQUTehdTfNycfUANEtCa9PssN9O/xmTzP4E08UI797ixaei6hBEVL9BI/PsdJS5x7mWoB9Q== + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.10.0.tgz#2cdcd6d7a391c24f7154235134c830cfb58ac0b1" + integrity sha512-tgYb3zVApHbLHYOPWtVwg25sBqHhfBXRKeKoTIyoheIxln1nA7oBl7SfHfiTG2GhDPI8EUBkOD/0wJCP/3HN4Q== dependencies: regenerator-runtime "^0.13.4" -"@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": - version "7.8.6" - resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" - integrity sha512-zbMsPMy/v0PWFZEhQJ66bqjhH+z0JgMoBWuikXybgG3Gkd/3t5oQ1Rw2WQhnSrsOmsKXnZOx15tkC4qON/+JPg== +"@babel/template@^7.10.0", "@babel/template@^7.4.0", "@babel/template@^7.8.3", "@babel/template@^7.8.6": + version "7.10.0" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.10.0.tgz#f15d852ce16cd5fb3e219097a75f662710b249b1" + integrity sha512-aMLEQn5tcG49LEWrsEwxiRTdaJmvLem3+JMCMSeCy2TILau0IDVyWdm/18ACx7XOCady64FLt6KkHy28tkDQHQ== dependencies: "@babel/code-frame" "^7.8.3" - "@babel/parser" "^7.8.6" - "@babel/types" "^7.8.6" + "@babel/parser" "^7.10.0" + "@babel/types" "^7.10.0" -"@babel/traverse@^7.1.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.3", "@babel/traverse@^7.8.6", "@babel/traverse@^7.9.0": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.9.5.tgz#6e7c56b44e2ac7011a948c21e283ddd9d9db97a2" - integrity sha512-c4gH3jsvSuGUezlP6rzSJ6jf8fYjLj3hsMZRx/nX0h+fmHN0w+ekubRrHPqnMec0meycA2nwCsJ7dC8IPem2FQ== - dependencies: - "@babel/code-frame" "^7.8.3" - "@babel/generator" "^7.9.5" - "@babel/helper-function-name" "^7.9.5" - "@babel/helper-split-export-declaration" "^7.8.3" - "@babel/parser" "^7.9.0" - "@babel/types" "^7.9.5" - debug "^4.1.0" - globals "^11.1.0" - lodash "^4.17.13" - -"@babel/traverse@^7.10.0": +"@babel/traverse@^7.1.0", "@babel/traverse@^7.10.0", "@babel/traverse@^7.4.3", "@babel/traverse@^7.8.3": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.10.0.tgz#290935529881baf619398d94fd453838bef36740" integrity sha512-NZsFleMaLF1zX3NxbtXI/JCs2RPOdpGru6UBdGsfhdsDsP+kFF+h2QQJnMJglxk0kc69YmMFs4A44OJY0tKo5g== @@ -854,16 +917,7 @@ globals "^11.1.0" lodash "^4.17.13" -"@babel/types@^7.0.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.8.6", "@babel/types@^7.9.0", "@babel/types@^7.9.5": - version "7.9.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.9.5.tgz#89231f82915a8a566a703b3b20133f73da6b9444" - integrity sha512-XjnvNqenk818r5zMaba+sLQjnbda31UfUURv3ei0qPQw4u+j2jMyJ5b11y8ZHYTRSI3NnInQkkkRT4fLqqPdHg== - dependencies: - "@babel/helper-validator-identifier" "^7.9.5" - lodash "^4.17.13" - to-fast-properties "^2.0.0" - -"@babel/types@^7.10.0": +"@babel/types@^7.0.0", "@babel/types@^7.10.0", "@babel/types@^7.3.0", "@babel/types@^7.4.0", "@babel/types@^7.4.4", "@babel/types@^7.8.3", "@babel/types@^7.9.0", "@babel/types@^7.9.5": version "7.10.0" resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.10.0.tgz#d47d92249e42393a5723aad5319035ae411e3e38" integrity sha512-t41W8yWFyQFPOAAvPvjyRhejcLGnJTA3iRpFcDbEKwVJ3UnHQePFzLk8GagTsucJlImyNwrGikGsYURrWbQG8w== @@ -1034,13 +1088,13 @@ "@types/yargs" "^13.0.0" "@rails/activestorage@^6.0.2-2": - version "6.0.2-2" - resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-6.0.2-2.tgz#108b0ad80a7cbf8d7e4c3ef5e54ea6ddb2a06d3c" - integrity sha512-bIQJqywfagz/UV0RP4jgLclUaBBsRoIdzGClaJ6z9EGa2QAnzu2Fdr7MrqlIDZl0/SZgteOmgF+WjZXNewSucA== + version "6.0.3" + resolved "https://registry.yarnpkg.com/@rails/activestorage/-/activestorage-6.0.3.tgz#401d2a28ecb7167cdb5e830ffddaa17c308c31aa" + integrity sha512-YdNwyfryHlcKj7Ruix89wZ2aiN3KTYULdW1Y/hNlHJlrY2/PXjT2YBTzZiVd+dcjrwHBsXV2rExdy+Z/lsrlEg== dependencies: spark-md5 "^3.0.0" -"@rails/webpacker@~5.1.1": +"@rails/webpacker@^5.1.1": version "5.1.1" resolved "https://registry.yarnpkg.com/@rails/webpacker/-/webpacker-5.1.1.tgz#3c937aa719e46341f037a3f37349ef58085950df" integrity sha512-ho5Stv9naZgG4HbHNFPqbA1OLPJyj6QXfgAc7VGCu4kkMe/RnVFLoLJFW6TZ9wYelKodBjRA2tKKiCaugv0sZw== @@ -1085,9 +1139,9 @@ webpack-sources "^1.4.3" "@sinonjs/commons@^1", "@sinonjs/commons@^1.3.0", "@sinonjs/commons@^1.7.0": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.7.2.tgz#505f55c74e0272b43f6c52d81946bed7058fc0e2" - integrity sha512-+DUO6pnp3udV/v2VfUWgaY5BIE1IfT7lLfeDzPVeMT1XKkaAp9LgSI9x5RtrFQoZ9Oi0PgXQQHPaoKu7dCjVxw== + version "1.8.0" + resolved "https://registry.yarnpkg.com/@sinonjs/commons/-/commons-1.8.0.tgz#c8d68821a854c555bba172f3b06959a0039b236d" + integrity sha512-wEj54PfsZ5jGSwMX68G8ZXFawcSglQSXqCftWX3ec8MDUzQdHgcKvw97awHbY0efQEL5iKUOAmmVtoYgmrSG4Q== dependencies: type-detect "4.0.8" @@ -1152,16 +1206,16 @@ "@babel/types" "^7.0.0" "@types/babel__traverse@*", "@types/babel__traverse@^7.0.6": - version "7.0.10" - resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.10.tgz#d9a99f017317d9b3d1abc2ced45d3bca68df0daf" - integrity sha512-74fNdUGrWsgIB/V9kTO5FGHPWYY6Eqn+3Z7L6Hc4e/BxjYV7puvBqp5HwsVYYfLm6iURYBNCx4Ut37OF9yitCw== + version "7.0.11" + resolved "https://registry.yarnpkg.com/@types/babel__traverse/-/babel__traverse-7.0.11.tgz#1ae3010e8bf8851d324878b42acec71986486d18" + integrity sha512-ddHK5icION5U6q11+tV2f9Mo6CZVuT8GJKld2q9LqHSZbvLbH34Kcu2yFGckZut453+eQU6btIA3RihmnRgI+Q== dependencies: "@babel/types" "^7.3.0" "@types/cheerio@*": - version "0.22.17" - resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.17.tgz#e54f71c3135f71ebc16c8dc62edad533872c9e72" - integrity sha512-izlm+hbqWN9csuB9GSMfCnAyd3/57XZi3rfz1B0C4QBGVMp+9xQ7+9KYnep+ySfUrCWql4lGzkLf0XmprXcz9g== + version "0.22.18" + resolved "https://registry.yarnpkg.com/@types/cheerio/-/cheerio-0.22.18.tgz#19018dceae691509901e339d63edf1e935978fe6" + integrity sha512-Fq7R3fINAPSdUEhOyjG4iVxgHrOnqDJbY0/BUuiN0pvD/rfmZWekVZnv+vcs8TtpA2XF50uv50LaE4EnpEL/Hw== dependencies: "@types/node" "*" @@ -1229,9 +1283,9 @@ hoist-non-react-statics "^3.3.0" "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": - version "2.0.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" - integrity sha512-hRJD2ahnnpLgsj6KWMYSrmXkM3rm2Dl1qkx6IOFD5FnuNPXJIG5L0dhgKXCYTRMGzU4n0wImQ/xfmRc4POUFlg== + version "2.0.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.2.tgz#79d7a78bad4219f4c03d6557a1c72d9ca6ba62d5" + integrity sha512-rsZg7eL+Xcxsxk2XlBt9KcG8nOp9iYdKCOikY9x2RFJCyOdNj4MKPQty0e8oZr29vVAzKXr1BmR+kZauti3o1w== "@types/istanbul-lib-report@*": version "3.0.0" @@ -1241,9 +1295,9 @@ "@types/istanbul-lib-coverage" "*" "@types/istanbul-reports@^1.1.1": - version "1.1.1" - resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.1.tgz#7a8cbf6a406f36c8add871625b278eaf0b0d255a" - integrity sha512-UpYjBi8xefVChsCoBpKShdxTllC9pwISirfoZsUa2AAdQg/Jd2KQGtSbw+ya7GPo7x/wAPlH6JBhKhAsXUEZNA== + version "1.1.2" + resolved "https://registry.yarnpkg.com/@types/istanbul-reports/-/istanbul-reports-1.1.2.tgz#e875cc689e47bce549ec81f3df5e6f6f11cfaeb2" + integrity sha512-P/W9yOX/3oPZSpaYOCQzGqgCQRXn0FFO/V8bWrCQs+wLmvVVxk6CRBXALEvNs9OHIatlnlFokfhuDo2ug01ciw== dependencies: "@types/istanbul-lib-coverage" "*" "@types/istanbul-lib-report" "*" @@ -1254,9 +1308,9 @@ integrity sha512-e74sM9W/4qqWB6D4TWV9FQk0WoHtX1X4FJpbjxucMSVJHtFjbQOH3H6yp+xno4br0AKG0wz/kPtaN599GUOvAg== "@types/jquery@^3.3.1": - version "3.3.35" - resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.35.tgz#ab2cbf97e7a04b4dc0faee22b93c633fa540891c" - integrity sha512-pnIELWhHXJ7RgoFylhiTxD+96QlKBJfEx8JCLj963/dh7zBOKFkZ6rlNqbaCcn2JZrsAxCI8WhgRXznBx2iDsA== + version "3.3.38" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-3.3.38.tgz#6385f1e1b30bd2bff55ae8ee75ea42a999cc3608" + integrity sha512-nkDvmx7x/6kDM5guu/YpXkGZ/Xj/IwGiLDdKM99YA5Vag7pjGyTJ8BNUh/6hxEn/sEu5DKtyRgnONJ7EmOoKrA== dependencies: "@types/sizzle" "*" @@ -1271,9 +1325,9 @@ parse5 "^4.0.0" "@types/lodash@^4.14.106": - version "4.14.150" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.150.tgz#649fe44684c3f1fcb6164d943c5a61977e8cf0bd" - integrity sha512-kMNLM5JBcasgYscD9x/Gvr6lTAv2NVgsKtet/hm93qMyf/D1pt+7jeEZklKJKxMVmXjxbRVQQGfqDSfipYCO6w== + version "4.14.153" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.153.tgz#5cb7dded0649f1df97938ac5ffc4f134e9e9df98" + integrity sha512-lYniGRiRfZf2gGAR9cfRC3Pi5+Q1ziJCKqPmjZocigrSJUVPWf7st1BtSJ8JOeK0FLXVndQ1IjUjTco9CXGo/Q== "@types/minimatch@*": version "3.0.3" @@ -1288,9 +1342,9 @@ moment ">=2.14.0" "@types/node@*": - version "13.13.1" - resolved "https://registry.yarnpkg.com/@types/node/-/node-13.13.1.tgz#1ba94c5a177a1692518bfc7b41aec0aa1a14354e" - integrity sha512-uysqysLJ+As9jqI5yqjwP3QJrhOcUwBjHUlUxPxjbplwKoILvXVsmYWEhfmAQlrPfbRZmhJB007o4L9sKqtHqQ== + version "14.0.5" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.0.5.tgz#3d03acd3b3414cf67faf999aed11682ed121f22b" + integrity sha512-90hiq6/VqtQgX8Sp0EzeIsv3r+ellbGj4URKj5j30tLlZvRUpnAe9YbYnjl3pJM93GyXU0tghHhvXHq+5rnCKA== "@types/parse-json@^4.0.0": version "4.0.0" @@ -1303,14 +1357,14 @@ integrity sha512-KfRL3PuHmqQLOG+2tGpRO26Ctg+Cq1E01D2DMriKEATHgWLfeNDmq9e29Q9WIky0dQ3NPkd1mzYH8Lm936Z9qw== "@types/q@^1.5.1": - version "1.5.2" - resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.2.tgz#690a1475b84f2a884fd07cd797c00f5f31356ea8" - integrity sha512-ce5d3q03Ex0sy4R14722Rmt6MT07Ua+k4FwDfdcToYJcMKNtRVQvJ6JCAPdAmAnbRb6CsX6aYb9m96NGod9uTw== + version "1.5.4" + resolved "https://registry.yarnpkg.com/@types/q/-/q-1.5.4.tgz#15925414e0ad2cd765bfef58842f7e26a7accb24" + integrity sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug== -"@types/react-dom@^16.9.4": - version "16.9.6" - resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.6.tgz#9e7f83d90566521cc2083be2277c6712dcaf754c" - integrity sha512-S6ihtlPMDotrlCJE9ST1fRmYrQNNwfgL61UB4I1W7M6kPulUKx9fXAleW5zpdIjUQ4fTaaog8uERezjsGUj9HQ== +"@types/react-dom@^16.9.8": + version "16.9.8" + resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" + integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== dependencies: "@types/react" "*" @@ -1340,10 +1394,10 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@^16.9.11": - version "16.9.34" - resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.34.tgz#f7d5e331c468f53affed17a8a4d488cd44ea9349" - integrity sha512-8AJlYMOfPe1KGLKyHpflCg5z46n0b5DbRfqDksxBLBTUpB75ypDBAO9eCUcjNwE6LCUslwTz00yyG/X9gaVtow== +"@types/react@*", "@types/react@^16.9.35": + version "16.9.35" + resolved "https://registry.yarnpkg.com/@types/react/-/react-16.9.35.tgz#a0830d172e8aadd9bd41709ba2281a3124bbd368" + integrity sha512-q0n0SsWcGc8nDqH2GJfWQWUOmZSJhXV64CjVN5SvcNti3TdEaA3AH0D8DwNmMdzjMAC/78tB8nAZIlV8yTz+zQ== dependencies: "@types/prop-types" "*" csstype "^2.2.0" @@ -1379,9 +1433,9 @@ integrity sha512-FA/BWv8t8ZWJ+gEOnLLd8ygxH/2UFbAvgEonyfN6yWGLKc7zVjbpl2Y4CTjid9h2RfgPP6SEt6uHwEOply00yw== "@types/yargs@^13.0.0": - version "13.0.8" - resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.8.tgz#a38c22def2f1c2068f8971acb3ea734eb3c64a99" - integrity sha512-XAvHLwG7UQ+8M4caKIH0ZozIOYay5fQkAgyIXegXT9jPtdIGdhga+sUEdAr1CiG46aB+c64xQEYyEzlwWVTNzA== + version "13.0.9" + resolved "https://registry.yarnpkg.com/@types/yargs/-/yargs-13.0.9.tgz#44028e974343c7afcf3960f1a2b1099c39a7b5e1" + integrity sha512-xrvhZ4DZewMDhoH1utLtOAwYQy60eYFoXeje30TzM3VOvQlBwQaEpKFq5m34k1wOw2AKIi2pwtiAjdmhvlBUzg== dependencies: "@types/yargs-parser" "*" @@ -1698,6 +1752,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@~3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.1.tgz#c55ecf02185e2469259399310c173ce31233b142" + integrity sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + aproba@^1.0.3, aproba@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/aproba/-/aproba-1.2.0.tgz#6802e6264efd18c790a1b0d517f0f2627bf2c94a" @@ -1895,22 +1957,22 @@ autoprefixer@^6.3.1: postcss-value-parser "^3.2.3" autoprefixer@^9.6.1: - version "9.7.6" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.7.6.tgz#63ac5bbc0ce7934e6997207d5bb00d68fa8293a4" - integrity sha512-F7cYpbN7uVVhACZTeeIeealwdGM6wMtfWARVLTy5xmKtgVdBNJvbDRoCK3YO1orcs7gv/KwYlb3iXwu9Ug9BkQ== + version "9.8.0" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-9.8.0.tgz#68e2d2bef7ba4c3a65436f662d0a56a741e56511" + integrity sha512-D96ZiIHXbDmU02dBaemyAg53ez+6F5yZmapmgKcjm35yEe1uVDYI8hGW3VYoGRaG290ZFf91YxHrR518vC0u/A== dependencies: - browserslist "^4.11.1" - caniuse-lite "^1.0.30001039" + browserslist "^4.12.0" + caniuse-lite "^1.0.30001061" chalk "^2.4.2" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^7.0.27" - postcss-value-parser "^4.0.3" + postcss "^7.0.30" + postcss-value-parser "^4.1.0" aws-sdk@^2.402.0: - version "2.660.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.660.0.tgz#1be2f814ffdb1aadf859b252601974073a39a4b2" - integrity sha512-6FR91Jg1x9TuFglsdBHkRuE4X7sPRwqeTB2GwLk9XPX1giicdMvJrWbcw5rUnMKjXs9LVlkwaK5VI9AJ0d8dpw== + version "2.685.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.685.0.tgz#ba5add21e98cc785b3c05ceb9f3fcb8ab046aa8a" + integrity sha512-mAOj7b4PuXRxIZkNdSkBWZ28lS2wYUY7O9u33nH9a7BawlttMNbxOgE/wDCPMrTLfj+RLQx0jvoIYj8BKCTRFw== dependencies: buffer "4.9.1" events "1.1.1" @@ -1928,9 +1990,9 @@ aws-sign2@~0.7.0: integrity sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg= aws4@^1.8.0: - version "1.9.1" - resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.1.tgz#7e33d8f7d449b3f673cd72deb9abdc552dbe528e" - integrity sha512-wMHVg2EOHaMRxbzgFJ9gtjOOCrI80OHLG14rxi28XwOW8ux6IiEbRCGGGqCtdAIg4FQCbW20k9RsT4y3gJlFug== + version "1.10.0" + resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.10.0.tgz#a17b3a8ea811060e74d47d306122400ad4497ae2" + integrity sha512-3YDiu347mtVtjpyV3u5kVqQLP242c06zwDOgpeRnybmXlYYsLbtTrUBUm8i8srONt+FWobl5aibnU1030PeeuA== babel-code-frame@^6.26.0: version "6.26.0" @@ -1965,7 +2027,7 @@ babel-loader@^8.1.0: pify "^4.0.1" schema-utils "^2.6.5" -babel-plugin-dynamic-import-node@^2.3.0: +babel-plugin-dynamic-import-node@^2.3.0, babel-plugin-dynamic-import-node@^2.3.3: version "2.3.3" resolved "https://registry.yarnpkg.com/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz#84fda19c976ec5c6defef57f9427b3def66e17a3" integrity sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ== @@ -1998,6 +2060,11 @@ babel-plugin-macros@^2.8.0: cosmiconfig "^6.0.0" resolve "^1.12.0" +babel-plugin-transform-react-remove-prop-types@^0.4.24: + version "0.4.24" + resolved "https://registry.yarnpkg.com/babel-plugin-transform-react-remove-prop-types/-/babel-plugin-transform-react-remove-prop-types-0.4.24.tgz#f2edaf9b4c6a5fbe5c1d678bfb531078c1555f3a" + integrity sha512-eqj0hVcJUR57/Ug2zE1Yswsw4LhuqqHhD+8v120T1cl3kjg76QwtyBrdIk4WVwK+lAhBJVYCd/v+4nc4y+8JsA== + babel-plugin-transform-runtime@^6.23.0: version "6.23.0" resolved "https://registry.yarnpkg.com/babel-plugin-transform-runtime/-/babel-plugin-transform-runtime-6.23.0.tgz#88490d446502ea9b8e7efb0fe09ec4d99479b1ee" @@ -2076,6 +2143,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bindings@^1.5.0: version "1.5.0" resolved "https://registry.yarnpkg.com/bindings/-/bindings-1.5.0.tgz#10353c9e945334bc0511a6d90b38fbc7c9c504df" @@ -2095,10 +2167,15 @@ bluebird@^3.5.5: resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== -bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: - version "4.11.8" - resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" - integrity sha512-ItfYfPLkWHUjckQCk8xC+LwxgK8NYcXywGigJgSwOP8Y2iyWT4f2vsZnoOXTTbo+o5yXmIUJ4gn5538SO5S3gA== +bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.4.0: + version "4.11.9" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828" + integrity sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw== + +bn.js@^5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.1.2.tgz#c9686902d3c9a27729f43ab10f9d79c2004da7b0" + integrity sha512-40rZaf3bUNKTVYu9sIeeEGOg7g14Yvnj9kH7b50EiwX0Q7A6umbvfI5tvHaOERH0XigqKkfLkFQxzb4e6CIXnA== body-parser@1.19.0: version "1.19.0" @@ -2180,6 +2257,13 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" +braces@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" + integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== + dependencies: + fill-range "^7.0.1" + brorand@^1.0.1: version "1.1.0" resolved "https://registry.yarnpkg.com/brorand/-/brorand-1.1.0.tgz#12c25efe40a45e3c323eb8675a0a0ce57b22371f" @@ -2233,7 +2317,7 @@ browserify-des@^1.0.0: inherits "^2.0.1" safe-buffer "^5.1.2" -browserify-rsa@^4.0.0: +browserify-rsa@^4.0.0, browserify-rsa@^4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/browserify-rsa/-/browserify-rsa-4.0.1.tgz#21e0abfaf6f2029cf2fafb133567a701d4135524" integrity sha1-IeCr+vbyApzy+vsTNWenAdQTVSQ= @@ -2242,17 +2326,19 @@ browserify-rsa@^4.0.0: randombytes "^2.0.1" browserify-sign@^4.0.0: - version "4.0.4" - resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.0.4.tgz#aa4eb68e5d7b658baa6bf6a57e630cbd7a93d298" - integrity sha1-qk62jl17ZYuqa/alfmMMvXqT0pg= + version "4.2.0" + resolved "https://registry.yarnpkg.com/browserify-sign/-/browserify-sign-4.2.0.tgz#545d0b1b07e6b2c99211082bf1b12cce7a0b0e11" + integrity sha512-hEZC1KEeYuoHRqhGhTy6gWrpJA3ZDjFWv0DE61643ZnOXAKJb3u7yWcrU0mMc9SwAqK1n7myPGndkp0dFG7NFA== dependencies: - bn.js "^4.1.1" - browserify-rsa "^4.0.0" - create-hash "^1.1.0" - create-hmac "^1.1.2" - elliptic "^6.0.0" - inherits "^2.0.1" - parse-asn1 "^5.0.0" + bn.js "^5.1.1" + browserify-rsa "^4.0.1" + create-hash "^1.2.0" + create-hmac "^1.1.7" + elliptic "^6.5.2" + inherits "^2.0.4" + parse-asn1 "^5.1.5" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" browserify-zlib@^0.2.0: version "0.2.0" @@ -2269,7 +2355,7 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" -browserslist@^4.0.0, browserslist@^4.11.1, browserslist@^4.6.4, browserslist@^4.8.5, browserslist@^4.9.1: +browserslist@^4.0.0, browserslist@^4.12.0, browserslist@^4.6.4, browserslist@^4.8.5: version "4.12.0" resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.12.0.tgz#06c6d5715a1ede6c51fc39ff67fd647f740b656d" integrity sha512-UH2GkcEDSI0k/lRkuDSzFl9ZZ87skSy9w2XAn1MsZnL+4c4rqbBd3e82UWHbYDpztABrPBhZsTEeuxVfHppqDg== @@ -2448,11 +2534,6 @@ camelcase@^2.0.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-2.1.1.tgz#7c1d16d679a1bbe59ca02cacecfb011e201f5a1f" integrity sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8= -camelcase@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" - integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= - camelcase@^4.1.0: version "4.1.0" resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" @@ -2489,14 +2570,14 @@ caniuse-api@^3.0.0: lodash.uniq "^4.5.0" caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: - version "1.0.30001045" - resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001045.tgz#87daa82cf21f0c4ba5d9ed75ba5dff6ff29fcf02" - integrity sha512-bwCb2ssU32vcxjG1P2VBOi1YV9W7G3RCG9m2++cX6Kql1oR+5w7vUBFL2SO47svJRUw+ai/d7yNT5VqoA2g3Sw== + version "1.0.30001067" + resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30001067.tgz#2cefe72e07490f2737b537f88e0dc42cf83c3fd8" + integrity sha512-idoz9VRtF3ycVF7NdeOkyHhmGBLZq3i4ib54v5LGEp/gnt/Vo0A2VWwF5zIigb/pIMzeCMqOMx2ioFfNh3P3EQ== -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001039, caniuse-lite@^1.0.30001043: - version "1.0.30001045" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001045.tgz#a770df9de36ad6ca0c34f90eaa797a2dbbb1b619" - integrity sha512-Y8o2Iz1KPcD6FjySbk1sPpvJqchgxk/iow0DABpGyzA1UeQAuxh63Xh0Enj5/BrsYbXtCN32JmR4ZxQTCQ6E6A== +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000981, caniuse-lite@^1.0.30001043, caniuse-lite@^1.0.30001061: + version "1.0.30001066" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001066.tgz#0a8a58a10108f2b9bf38e7b65c237b12fd9c5f04" + integrity sha512-Gfj/WAastBtfxLws0RCh2sDbTK/8rJuSeZMecrSkNGYxPcv7EzblmDGfWQCFEQcSqYE2BRgQiJh8HOD07N5hIw== capture-exit@^2.0.0: version "2.0.0" @@ -2598,6 +2679,21 @@ chokidar@^2.1.8: optionalDependencies: fsevents "^1.2.7" +chokidar@^3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.4.0.tgz#b30611423ce376357c765b9b8f904b9fba3c0be8" + integrity sha512-aXAaho2VJtisB/1fg1+3nlLJqGOuewTzQpd/Tz0yTg2R0e4IGtshYvtjowyEumcBv2z+y4+kc75Mz7j5xJskcQ== + dependencies: + anymatch "~3.1.1" + braces "~3.0.2" + glob-parent "~5.1.0" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.4.0" + optionalDependencies: + fsevents "~2.1.2" + chownr@^1.1.1, chownr@^1.1.2: version "1.1.4" resolved "https://registry.yarnpkg.com/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" @@ -2650,15 +2746,6 @@ clean-stack@^2.0.0: resolved "https://registry.yarnpkg.com/clean-stack/-/clean-stack-2.2.0.tgz#ee8472dbb129e727b31e8a10a427dee9dfe4008b" integrity sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A== -cliui@^3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/cliui/-/cliui-3.2.0.tgz#120601537a916d29940f934da3b48d585a39213d" - integrity sha1-EgYBU3qRbSmUD5NNo7SNWFo5IT0= - dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" - wrap-ansi "^2.0.0" - cliui@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-5.0.0.tgz#deefcfdb2e800784aa34f46fa08e06851c7bbbc5" @@ -3011,7 +3098,7 @@ create-ecdh@^4.0.0: bn.js "^4.1.0" elliptic "^6.0.0" -create-hash@^1.1.0, create-hash@^1.1.2: +create-hash@^1.1.0, create-hash@^1.1.2, create-hash@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/create-hash/-/create-hash-1.2.0.tgz#889078af11a63756bcfb59bd221996be3a9ef196" integrity sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg== @@ -3022,7 +3109,7 @@ create-hash@^1.1.0, create-hash@^1.1.2: ripemd160 "^2.0.1" sha.js "^2.4.0" -create-hmac@^1.1.0, create-hmac@^1.1.2, create-hmac@^1.1.4: +create-hmac@^1.1.0, create-hmac@^1.1.4, create-hmac@^1.1.7: version "1.1.7" resolved "https://registry.yarnpkg.com/create-hmac/-/create-hmac-1.1.7.tgz#69170c78b3ab957147b2b8b04572e47ead2243ff" integrity sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg== @@ -3131,9 +3218,9 @@ css-loader@^0.28.10: source-list-map "^2.0.0" css-loader@^3.4.2: - version "3.5.2" - resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.2.tgz#6483ae56f48a7f901fbe07dde2fc96b01eafab3c" - integrity sha512-hDL0DPopg6zQQSRlZm0hyeaqIRnL0wbWjay9BZxoiJBpbfOW4WHfbaYQhwnDmEa0kZUc1CJ3IFo15ot1yULMIQ== + version "3.5.3" + resolved "https://registry.yarnpkg.com/css-loader/-/css-loader-3.5.3.tgz#95ac16468e1adcd95c844729e0bb167639eb0bcf" + integrity sha512-UEr9NH5Lmi7+dguAm+/JSPovNjYbm2k3TK58EiwQHzOHH5Jfq1Y+XoP2bQO6TMn7PptMd0opxxedAWcaSTRKHw== dependencies: camelcase "^5.3.1" cssesc "^3.0.0" @@ -3146,7 +3233,7 @@ css-loader@^3.4.2: postcss-modules-scope "^2.2.0" postcss-modules-values "^3.0.0" postcss-value-parser "^4.0.3" - schema-utils "^2.6.5" + schema-utils "^2.6.6" semver "^6.3.0" css-prefers-color-scheme@^3.1.1: @@ -3438,7 +3525,7 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -decamelize@^1.1.1, decamelize@^1.1.2, decamelize@^1.2.0: +decamelize@^1.1.2, decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha1-9lNNFRSCabIDUue+4m9QH5oZEpA= @@ -3736,11 +3823,11 @@ ee-first@1.1.1: integrity sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0= electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.413: - version "1.3.414" - resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.414.tgz#9d0a92defefda7cc1cf8895058b892795ddd6b41" - integrity sha512-UfxhIvED++qLwWrAq9uYVcqF8FdeV9sU2S7qhiHYFODxzXRrd1GZRl/PjITHsTEejgibcWDraD8TQqoHb1aCBQ== + version "1.3.453" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.453.tgz#758a8565a64b7889b27132a51d2abb8b135c9d01" + integrity sha512-IQbCfjJR0NDDn/+vojTlq7fPSREcALtF8M1n01gw7nQghCtfFYrJ2dfhsp8APr8bANoFC8vRTFVXMOGpT0eetw== -elliptic@^6.0.0: +elliptic@^6.0.0, elliptic@^6.5.2: version "6.5.2" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.5.2.tgz#05c5678d7173c049d8ca433552224a495d0e3762" integrity sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw== @@ -3804,9 +3891,9 @@ entities@^1.1.1, entities@~1.1.1: integrity sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w== entities@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.0.tgz#68d6084cab1b079767540d80e56a39b423e4abf4" - integrity sha512-D9f7V0JSRwIxlRI2mjMqufDrRDnx8p+eEOz7aUM9SuvF8gsBzra0/6tbjl1m8eQHrZlYj6PxqE00hZ1SAIKPLw== + version "2.0.2" + resolved "https://registry.yarnpkg.com/entities/-/entities-2.0.2.tgz#ac74db0bba8d33808bbf36809c3a5c3683531436" + integrity sha512-dmD3AvJQBUjKpcNkoqr+x+IF0SdRtPz9Vk0uTy4yWqga9ibB6s4v++QFWNohjiUGoMlF552ZvNyXDxz5iW0qmw== enzyme-adapter-react-16@^1.9.1: version "1.15.2" @@ -3852,9 +3939,9 @@ enzyme-shallow-equal@^1.0.1: object-is "^1.0.2" enzyme-to-json@^3.3.0, enzyme-to-json@^3.3.3: - version "3.4.4" - resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.4.4.tgz#b30726c59091d273521b6568c859e8831e94d00e" - integrity sha512-50LELP/SCPJJGic5rAARvU7pgE3m1YaNj7JLM+Qkhl5t7PAs6fiyc8xzc50RnkKPFQCv0EeFVjEWdIFRGPWMsA== + version "3.5.0" + resolved "https://registry.yarnpkg.com/enzyme-to-json/-/enzyme-to-json-3.5.0.tgz#3d536f1e8fb50d972360014fe2bd64e6a672f7dd" + integrity sha512-clusXRsiaQhG7+wtyc4t7MU8N3zCOgf4eY9+CeSenYzKlFST4lxerfOvnWd4SNaToKhkuba+w6m242YpQOS7eA== dependencies: lodash "^4.17.15" react-is "^16.12.0" @@ -4031,9 +4118,9 @@ eventemitter3@^2.0.3: integrity sha1-teEHm1n7XhuidxwKmTvgYKWMmbo= eventemitter3@^4.0.0: - version "4.0.0" - resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" - integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== + version "4.0.4" + resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-4.0.4.tgz#b5463ace635a083d018bdc7c917b4c5f10a85384" + integrity sha512-rlaVLnVxtxvoyLsQQFBx53YmXHDxRIzzTLbdfxqi4yocpSjAxXwkU0cScM5JgSKMqEhrZpnvQ2D9gjylR0AimQ== events@1.1.1: version "1.1.1" @@ -4333,6 +4420,13 @@ fill-range@^4.0.0: repeat-string "^1.6.1" to-regex-range "^2.1.0" +fill-range@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.0.1.tgz#1919a6a7c75fe38b2c7c77e5198535da9acdda40" + integrity sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ== + dependencies: + to-regex-range "^5.0.1" + finalhandler@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.2.tgz#b7e7d000ffd11938d0fdb053506f6ebabe9f587d" @@ -4355,7 +4449,7 @@ find-cache-dir@^2.1.0: make-dir "^2.0.0" pkg-dir "^3.0.0" -find-cache-dir@^3.0.0, find-cache-dir@^3.2.0: +find-cache-dir@^3.0.0, find-cache-dir@^3.3.1: version "3.3.1" resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-3.3.1.tgz#89b33fad4a4670daa94f855f7fbe31d6d84fe880" integrity sha512-t2GDMt3oGC/v+BMwzmllWDuJF/xcDtE5j/fCGbqDD7OLuJkj0cfh1YSA5VKPvwMeLFLNDBkwOKZ2X85jGLVftQ== @@ -4580,6 +4674,19 @@ forever-agent@~0.6.1: resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" integrity sha1-+8cfDEGt6zf5bFd60e1C2P2sypE= +fork-ts-checker-webpack-plugin@^4.1.6: + version "4.1.6" + resolved "https://registry.yarnpkg.com/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.6.tgz#5055c703febcf37fa06405d400c122b905167fc5" + integrity sha512-DUxuQaKoqfNne8iikd14SAkh5uw4+8vNifp6gmA73yYNS6ywLIWSLD/n/mBzHQRpW3J7rbATEakmiA8JvkTyZw== + dependencies: + "@babel/code-frame" "^7.5.5" + chalk "^2.4.1" + micromatch "^3.1.10" + minimatch "^3.0.4" + semver "^5.6.0" + tapable "^1.0.0" + worker-rpc "^0.1.0" + form-data@0.1.3: version "0.1.3" resolved "https://registry.yarnpkg.com/form-data/-/form-data-0.1.3.tgz#4ee4346e6eb5362e8344a02075bd8dbd8c7373ea" @@ -4680,13 +4787,18 @@ fs.realpath@^1.0.0: integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= fsevents@^1.2.7: - version "1.2.12" - resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.12.tgz#db7e0d8ec3b0b45724fd4d83d43554a8f1f0de5c" - integrity sha512-Ggd/Ktt7E7I8pxZRbGIs7vwqAPscSESMrCSkx2FtWeqmheJgCo2R74fTsZFCifr0VTPwqRpPv17+6b8Zp7th0Q== + version "1.2.13" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-1.2.13.tgz#f325cb0455592428bcf11b383370ef70e3bfcc38" + integrity sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw== dependencies: bindings "^1.5.0" nan "^2.12.1" +fsevents@~2.1.2: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.1.3.tgz#fb738703ae8d2f9fe900c33836ddebee8b97f23e" + integrity sha512-Auw9a4AxqWpa9GUfj370BMPzzyncfBABW8Mab7BGWBYDj4Isgq+cDKtx0i6u9jcX9pQDnswsaaOTgTmA5pEjuQ== + fstream@^1.0.0, fstream@^1.0.12: version "1.0.12" resolved "https://registry.yarnpkg.com/fstream/-/fstream-1.0.12.tgz#4e8ba8ee2d48be4f7d0de505455548eae5932045" @@ -4742,11 +4854,6 @@ gensync@^1.0.0-beta.1: resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" integrity sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg== -get-caller-file@^1.0.1: - version "1.0.3" - resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.3.tgz#f978fa4c90d1dfe7ff2d6beda2a515e713bdcf4a" - integrity sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w== - get-caller-file@^2.0.1: version "2.0.5" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e" @@ -4784,6 +4891,13 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" +glob-parent@~5.1.0: + version "5.1.1" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" + integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== + dependencies: + is-glob "^4.0.1" + glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@~7.1.1: version "7.1.6" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.6.tgz#141f33b81a7c2492e125594307480c46679278a6" @@ -4866,9 +4980,9 @@ globule@^1.0.0: minimatch "~3.0.2" graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.6, graceful-fs@^4.1.9, graceful-fs@^4.2.2: - version "4.2.3" - resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.3.tgz#4a12ff1b60376ef09862c2093edd908328be8423" - integrity sha512-a30VEBm4PEdx1dRB7MFK7BejejvCvBronbLjht+sHuGYj8PHs7M/5Z+rt5lw551vZ7yfTCj4Vuyy3mSJytDWRQ== + version "4.2.4" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" + integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== growly@^1.3.0: version "1.3.0" @@ -4964,12 +5078,13 @@ has@^1.0.0, has@^1.0.1, has@^1.0.3: function-bind "^1.1.1" hash-base@^3.0.0: - version "3.0.4" - resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.0.4.tgz#5fc8686847ecd73499403319a6b0a3f3f6ae4918" - integrity sha1-X8hoaEfs1zSZQDMZprCj8/auSRg= + version "3.1.0" + resolved "https://registry.yarnpkg.com/hash-base/-/hash-base-3.1.0.tgz#55c381d9e06e1d2997a883b4a3fddfe7f0d3af33" + integrity sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA== dependencies: - inherits "^2.0.1" - safe-buffer "^5.0.1" + inherits "^2.0.4" + readable-stream "^3.6.0" + safe-buffer "^5.2.0" hash.js@^1.0.0, hash.js@^1.0.3: version "1.1.7" @@ -5118,10 +5233,10 @@ http-errors@~1.7.2: statuses ">= 1.5.0 < 2" toidentifier "1.0.0" -"http-parser-js@>=0.4.0 <0.4.11": - version "0.4.10" - resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" - integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= +http-parser-js@>=0.5.1: + version "0.5.2" + resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.2.tgz#da2e31d237b393aae72ace43882dd7e270a8ff77" + integrity sha512-opCO9ASqg5Wy2FNo7A0sxy71yGbbkJJXLdgMK04Tcypw9jr2MgWbyubb0+WdmDmGnFflO7fRbqbaihh/ENDlRQ== http-proxy-middleware@0.19.1: version "0.19.1" @@ -5134,9 +5249,9 @@ http-proxy-middleware@0.19.1: micromatch "^3.1.10" http-proxy@^1.17.0: - version "1.18.0" - resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.0.tgz#dbe55f63e75a347db7f3d99974f2692a314a6a3a" - integrity sha512-84I2iJM/n1d4Hdgc6y2+qY5mDaz2PUVjlg9znE9byl+q0uC3DeByqBGReQu5tpLK0TAqTIXScRUV+dg7+bUPpQ== + version "1.18.1" + resolved "https://registry.yarnpkg.com/http-proxy/-/http-proxy-1.18.1.tgz#401541f0534884bbf95260334e72f88ee3976549" + integrity sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ== dependencies: eventemitter3 "^4.0.0" follow-redirects "^1.0.0" @@ -5297,7 +5412,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5361,11 +5476,6 @@ invariant@^2.1.1, invariant@^2.2.2, invariant@^2.2.4: dependencies: loose-envify "^1.0.0" -invert-kv@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" - integrity sha1-EEqOSqym09jNFXqO+L+rLXo//bY= - invert-kv@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-2.0.0.tgz#7393f5afa59ec9ff5f67a27620d11c226e3eec02" @@ -5432,6 +5542,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-boolean-object@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.0.1.tgz#10edc0900dd127697a92f6f9807c7617d68ac48e" @@ -5554,7 +5671,7 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" -is-glob@^4.0.0: +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -5573,6 +5690,11 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + is-obj@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-2.0.0.tgz#473fb05d973705e3fd9620545018ca8e22ef4982" @@ -6121,10 +6243,10 @@ jest-worker@^24.6.0, jest-worker@^24.9.0: merge-stream "^2.0.0" supports-color "^6.1.0" -jest-worker@^25.1.0: - version "25.4.0" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.4.0.tgz#ee0e2ceee5a36ecddf5172d6d7e0ab00df157384" - integrity sha512-ghAs/1FtfYpMmYQ0AHqxV62XPvKdUDIBBApMZfly+E9JEmYh2K45G0R5dWxx986RN12pRCxsViwQVtGl+N4whw== +jest-worker@^25.4.0: + version "25.5.0" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-25.5.0.tgz#2611d071b79cea0f43ee57a3d118593ac1547db1" + integrity sha512-/dsSmUkIy5EBGfv/IjjqmFxrNAUpBERfGs1oHROyD7yxjG/w+t0GOJDX8O1k32ySmd7+a5IhnJU2qQFcJ4n1vw== dependencies: merge-stream "^2.0.0" supports-color "^7.0.0" @@ -6176,9 +6298,9 @@ js-yaml@3.11.0: esprima "^4.0.0" js-yaml@^3.13.1: - version "3.13.1" - resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.13.1.tgz#aff151b30bfdfa8e49e05da22e7415e9dfa37847" - integrity sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw== + version "3.14.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.14.0.tgz#a7a34170f26a21bb162424d8adacb4113a69e482" + integrity sha512-/4IbIeHcD9VMHFqDR/gQ7EdZdLimOvW2DdcxFjdyyZ9NsbS+ccrXqVWDtab/lRl5AlUqmpBx8EhPaWR+OtY17A== dependencies: argparse "^1.0.7" esprima "^4.0.0" @@ -6358,13 +6480,6 @@ last-call-webpack-plugin@^3.0.0: lodash "^4.17.5" webpack-sources "^1.1.0" -lcid@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/lcid/-/lcid-1.0.0.tgz#308accafa0bc483a3867b4b6f2b9506251d1b835" - integrity sha1-MIrMr6C8SDo4Z7S28rlQYlHRuDU= - dependencies: - invert-kv "^1.0.0" - lcid@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lcid/-/lcid-2.0.0.tgz#6ef5d2df60e52f82eb228a4c373e8d1f397253cf" @@ -6728,9 +6843,9 @@ make-dir@^2.0.0, make-dir@^2.1.0: semver "^5.6.0" make-dir@^3.0.2: - version "3.0.2" - resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.0.2.tgz#04a1acbf22221e1d6ef43559f43e05a90dbb4392" - integrity sha512-rYKABKutXa6vXTXhoV18cBE7PaewPXHe/Bdq4v+ZLMhxbWApkFFplT0LcbMW+6BbjnQXzZ/sAvSE/JdguApG5w== + version "3.1.0" + resolved "https://registry.yarnpkg.com/make-dir/-/make-dir-3.1.0.tgz#415e967046b3a7f1d185277d84aa58203726a13f" + integrity sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw== dependencies: semver "^6.0.0" @@ -6865,6 +6980,11 @@ methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4= +microevent.ts@~0.1.1: + version "0.1.1" + resolved "https://registry.yarnpkg.com/microevent.ts/-/microevent.ts-0.1.1.tgz#70b09b83f43df5172d0205a63025bce0f7357fa0" + integrity sha512-jo1OfR4TaEwd5HOrt5+tAZ9mqT4jmpNAusXtyfNzqVm9uiSYFZlKM1wYL4oU7azZW/PxQW53wM0S6OR1JHNa2g== + micromatch@^3.0.4, micromatch@^3.1.10, micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -6892,17 +7012,17 @@ miller-rabin@^4.0.0: bn.js "^4.0.0" brorand "^1.0.1" -mime-db@1.43.0, "mime-db@>= 1.43.0 < 2": - version "1.43.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.43.0.tgz#0a12e0502650e473d735535050e7c8f4eb4fae58" - integrity sha512-+5dsGEEovYbT8UY9yD7eE4XTc4UwJ1jBYlgaQQF38ENsKR3wj/8q8RFZrF9WIZpB2V1ArTVFUva8sAul1NzRzQ== +mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": + version "1.44.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" + integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: - version "2.1.26" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.26.tgz#9c921fc09b7e149a65dfdc0da4d20997200b0a06" - integrity sha512-01paPWYgLrkqAyrlDorC1uDwl2p3qZT7yl806vW7DvDoxwXi46jsjFbg+WdwotBIk6/MbEhO/dh5aZ5sNj/dWQ== + version "2.1.27" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" + integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== dependencies: - mime-db "1.43.0" + mime-db "1.44.0" mime@1.2.11, mime@~1.2.11: version "1.2.11" @@ -6915,9 +7035,9 @@ mime@1.6.0: integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg== mime@^2.0.3, mime@^2.4.4: - version "2.4.4" - resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.4.tgz#bd7b91135fc6b01cde3e9bae33d659b63d8857e5" - integrity sha512-LRxmNwziLPT828z+4YkNzloCFC2YM4wrB99k+AV5ZbEyfGNWfG8SO1FUXLmLDBSo89NrJZ4DIWeLjy1CHGhMGA== + version "2.4.6" + resolved "https://registry.yarnpkg.com/mime/-/mime-2.4.6.tgz#e5b407c90db442f2beb5b162373d07b69affa4d1" + integrity sha512-RZKhC3EmpBchfTGBVb8fb+RL2cWyw/32lshnsETttkBAyAUXSGHxbEJWWRXc751DrIxG1q04b8QwMbAwkRPpUA== mimic-fn@^2.0.0: version "2.1.0" @@ -6988,16 +7108,16 @@ minipass-flush@^1.0.5: minipass "^3.0.0" minipass-pipeline@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz#3dcb6bb4a546e32969c7ad710f2c79a86abba93a" - integrity sha512-3JS5A2DKhD2g0Gg8x3yamO0pj7YeKGwVlDS90pF++kxptwx/F+B//roxf9SqYil5tQo65bijy+dAuAFZmYOouA== + version "1.2.3" + resolved "https://registry.yarnpkg.com/minipass-pipeline/-/minipass-pipeline-1.2.3.tgz#55f7839307d74859d6e8ada9c3ebe72cec216a34" + integrity sha512-cFOknTvng5vqnwOpDsZTWhNll6Jf8o2x+/diplafmxpuIymAjzoOolZG0VvQf3V2HgqzJNhnuKHYp2BqDgz8IQ== dependencies: minipass "^3.0.0" minipass@^3.0.0, minipass@^3.1.1: - version "3.1.1" - resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.1.tgz#7607ce778472a185ad6d89082aa2070f79cedcd5" - integrity sha512-UFqVihv6PQgwj8/yTGvl9kPz7xIAY+R5z6XYjRInD3Gk3qx6QGSD6zEcpeG4Dy/lQnv1J6zv8ejV90hyYIKf3w== + version "3.1.3" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== dependencies: yallist "^4.0.0" @@ -7053,9 +7173,9 @@ mobx-react@^5.4.3: react-lifecycles-compat "^3.0.2" mobx-utils@^5.0.1: - version "5.5.7" - resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-5.5.7.tgz#0ef58f2d5e05ca0e59ba2322f84f9c763de6ce14" - integrity sha512-jEtTe45gCXYtv3WTAyPiQUhQQRRDnx68WxgNn886i1B11ormsAey+gIJJkfh/cqSssBEWXcXwYTvODpGPN8Tgw== + version "5.6.1" + resolved "https://registry.yarnpkg.com/mobx-utils/-/mobx-utils-5.6.1.tgz#b7d9184b7442fe704be367d4363a2e9961be28cc" + integrity sha512-bpTJzM8MXniGnXCZY+ImjPDqBKQ3+G3g/QFSPtNkH6HM3x14DAPqKH7No7NDyhbXBMv3FaVetsgnoEPozbi45Q== mobx@^4.3.1: version "4.15.4" @@ -7068,16 +7188,16 @@ moment-range@2.2.0: integrity sha1-sCV1d4pKxQpld3k59cXXV/g/zYU= moment-timezone@^0.5.21: - version "0.5.28" - resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.28.tgz#f093d789d091ed7b055d82aa81a82467f72e4338" - integrity sha512-TDJkZvAyKIVWg5EtVqRzU97w0Rb0YVbfpqyjgu6GwXCAohVRqwZjf4fOzDE6p1Ch98Sro/8hQQi65WDXW5STPw== + version "0.5.31" + resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.31.tgz#9c40d8c5026f0c7ab46eda3d63e49c155148de05" + integrity sha512-+GgHNg8xRhMXfEbv81iDtrVeTcWt0kWmTEY1XQK14dICTXnWJnT0dxdlPspwqF3keKMVPXwayEsk1DI0AA/jdA== dependencies: moment ">= 2.9.0" moment@2.x, "moment@>= 2.9.0", moment@>=2.14.0, moment@^2.10.6, moment@^2.22.2: - version "2.24.0" - resolved "https://registry.yarnpkg.com/moment/-/moment-2.24.0.tgz#0d055d53f5052aa653c9f6eb68bb5d12bf5c2b5b" - integrity sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg== + version "2.26.0" + resolved "https://registry.yarnpkg.com/moment/-/moment-2.26.0.tgz#5e1f82c6bafca6e83e808b30c8705eed0dcbd39a" + integrity sha512-oIixUO+OamkUkwjhAVE18rAMfRJNsNe/Stid/gwHSOfHrOtw9EhAY2AHvdKZ/k/MggcYELFCJz/Sn2pL8b8JMw== moo@^0.5.0: version "0.5.1" @@ -7152,9 +7272,9 @@ natural-compare@^1.4.0: integrity sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc= nearley@^2.7.10: - version "2.19.2" - resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.2.tgz#40cafbf235121ae94b1aa1e585890d24fade182d" - integrity sha512-h6lygT0BWAGErDvoE2LfI+tDeY2+UUrqG5dcBPdCmjnjud9z1wE0P7ljb85iNbE93YA+xJLpoSYGMuUqhnSSSA== + version "2.19.3" + resolved "https://registry.yarnpkg.com/nearley/-/nearley-2.19.3.tgz#ae3b040e27616b5348102c436d1719209476a5a1" + integrity sha512-FpAy1PmTsUpOtgxr23g4jRNvJHYzZEW2PixXeSzksLR/ykPfwKhAodc2+9wQhY+JneWLcvkDw6q7FJIsIdF/aQ== dependencies: commander "^2.19.0" moo "^0.5.0" @@ -7279,14 +7399,14 @@ node-notifier@^5.4.2: which "^1.3.0" node-releases@^1.1.53: - version "1.1.53" - resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.53.tgz#2d821bfa499ed7c5dffc5e2f28c88e78a08ee3f4" - integrity sha512-wp8zyQVwef2hpZ/dJH7SfSrIPD6YoJz6BDQDpGEkcA0s3LpAQoxBIYmfIq6QAhC1DhwsyCgTaTTcONwX8qzCuQ== + version "1.1.57" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-1.1.57.tgz#f6754ce225fad0611e61228df3e09232e017ea19" + integrity sha512-ZQmnWS7adi61A9JsllJ2gdj2PauElcjnOwTp2O011iGzoakTxUsDGSe+6vD7wXbKdqhSFymC0OSx35aAMhrSdw== node-sass@^4.12.0, node-sass@^4.13.1: - version "4.13.1" - resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.13.1.tgz#9db5689696bb2eec2c32b98bfea4c7a2e992d0a3" - integrity sha512-TTWFx+ZhyDx1Biiez2nB0L3YrCZ/8oHagaDalbuBSlqXgUPsdkUSzJsVxeDO9LtPB49+Fh3WQl3slABo6AotNw== + version "4.14.1" + resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.14.1.tgz#99c87ec2efb7047ed638fb4c9db7f3a42e2217b5" + integrity sha512-sjCuOlvGyCJS40R8BscF5vhVlQjNN069NtQ1gSxyK1u9iqvn6tf7O1R4GNowVZfiZUCRt5MmMs1xd+4V/7Yr0g== dependencies: async-foreach "^0.1.3" chalk "^1.1.1" @@ -7302,7 +7422,7 @@ node-sass@^4.12.0, node-sass@^4.13.1: node-gyp "^3.8.0" npmlog "^4.0.0" request "^2.88.0" - sass-graph "^2.2.4" + sass-graph "2.2.5" stdout-stream "^1.4.0" "true-case-path" "^1.0.2" @@ -7330,7 +7450,7 @@ normalize-path@^2.1.1: dependencies: remove-trailing-separator "^1.0.1" -normalize-path@^3.0.0: +normalize-path@^3.0.0, normalize-path@~3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== @@ -7454,13 +7574,12 @@ object.assign@^4.1.0: object-keys "^1.0.11" object.entries@^1.1.0, object.entries@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.1.tgz#ee1cf04153de02bb093fec33683900f57ce5399b" - integrity sha512-ilqR7BgdyZetJutmDPfXCDffGa0/Yzl2ivVNpbx/g4UeWrCdRnFDUBrKJGLhGieRHDATnyZXWBeCb29k9CJysQ== + version "1.1.2" + resolved "https://registry.yarnpkg.com/object.entries/-/object.entries-1.1.2.tgz#bc73f00acb6b6bb16c203434b10f9a7e797d3add" + integrity sha512-BQdB9qKmb/HyNdMNWVr7O3+z5MUIx3aiegEIJqjMBbBf0YT9RRxTJSim4mzFqtyr7PDAHigq0N9dO0m0tRakQA== dependencies: define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" - function-bind "^1.1.1" + es-abstract "^1.17.5" has "^1.0.3" object.fromentries@^2.0.2: @@ -7566,13 +7685,6 @@ os-homedir@^1.0.0: resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" integrity sha1-/7xJiDNuDoM94MFox+8VISGqf7M= -os-locale@^1.4.0: - version "1.4.0" - resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-1.4.0.tgz#20f9f17ae29ed345e8bde583b13d2009803c14d9" - integrity sha1-IPnxeuKe00XoveWDsT0gCYA8FNk= - dependencies: - lcid "^1.0.0" - os-locale@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" @@ -7624,7 +7736,7 @@ p-limit@^1.1.0: dependencies: p-try "^1.0.0" -p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.2.2: +p-limit@^2.0.0, p-limit@^2.2.0, p-limit@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1" integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w== @@ -7712,7 +7824,7 @@ parent-module@^1.0.0: dependencies: callsites "^3.0.0" -parse-asn1@^5.0.0: +parse-asn1@^5.0.0, parse-asn1@^5.1.5: version "5.1.5" resolved "https://registry.yarnpkg.com/parse-asn1/-/parse-asn1-5.1.5.tgz#003271343da58dc94cace494faef3d2147ecea0e" integrity sha512-jkMYn1dcJqF6d5CpU689bq7w/b5ALS9ROVSpQDPrZsqqesUJii9qutvoT5ltGedNXMO2e16YUWIghG9KxaViTQ== @@ -7912,6 +8024,11 @@ phone-formatter@0.0.2: resolved "https://registry.yarnpkg.com/phone-formatter/-/phone-formatter-0.0.2.tgz#f3626c7d274860f014f70f43a87566a16b0e7ace" integrity sha1-82JsfSdIYPAU9w9DqHVmoWsOes4= +picomatch@^2.0.4, picomatch@^2.2.1: + version "2.2.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.2.2.tgz#21f333e9b6b8eaff02468f5146ea406d345f4dad" + integrity sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg== + pify@^2.0.0, pify@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" @@ -8230,9 +8347,9 @@ postcss-filter-plugins@^2.0.0: postcss "^5.0.4" postcss-flexbugs-fixes@^4.2.0: - version "4.2.0" - resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.0.tgz#662b3dcb6354638b9213a55eed8913bcdc8d004a" - integrity sha512-QRE0n3hpkxxS/OGvzOa+PDuy4mh/Jg4o9ui22/ko5iGYOG3M5dfJabjnAZjTdh2G9F85c7Hv8hWcEDEKW/xceQ== + version "4.2.1" + resolved "https://registry.yarnpkg.com/postcss-flexbugs-fixes/-/postcss-flexbugs-fixes-4.2.1.tgz#9218a65249f30897deab1033aced8578562a6690" + integrity sha512-9SiofaZ9CWpQWxOwRh1b/r85KD5y7GgvsNt1056k6OYLvWUun0czCvogfJgylC22uJTwW1KzY3Gz65NZRlvoiQ== dependencies: postcss "^7.0.26" @@ -8874,10 +8991,10 @@ postcss-value-parser@^3.0.0, postcss-value-parser@^3.0.1, postcss-value-parser@^ resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz#9ff822547e2893213cf1c30efa51ac5fd1ba8281" integrity sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ== -postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3: - version "4.0.3" - resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.0.3.tgz#651ff4593aa9eda8d5d0d66593a2417aeaeb325d" - integrity sha512-N7h4pG+Nnu5BEIzyeaaIYWs0LI5XC40OrRh5L60z0QjFsqGWcHcbkBvpe1WYpcIS9yQ8sOi/vIPt1ejQCrMVrg== +postcss-value-parser@^4.0.0, postcss-value-parser@^4.0.2, postcss-value-parser@^4.0.3, postcss-value-parser@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz#443f6a20ced6481a2bda4fa8532a6e55d789a2cb" + integrity sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ== postcss-values-parser@^2.0.0, postcss-values-parser@^2.0.1: version "2.0.1" @@ -8916,10 +9033,10 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" -postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.5, postcss@^7.0.6: - version "7.0.27" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.27.tgz#cc67cdc6b0daa375105b7c424a85567345fc54d9" - integrity sha512-WuQETPMcW9Uf1/22HWUWP9lgsIC+KEHg2kozMflKjbeUtw9ujvFX6QmIfozaErDkmLWS9WEnEdEe6Uo9/BNTdQ== +postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.16, postcss@^7.0.17, postcss@^7.0.2, postcss@^7.0.26, postcss@^7.0.27, postcss@^7.0.30, postcss@^7.0.5, postcss@^7.0.6: + version "7.0.31" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-7.0.31.tgz#332af45cb73e26c0ee2614d7c7fb02dfcc2bd6dd" + integrity sha512-a937VDHE1ftkjk+8/7nj/mrjtmkn69xxzJgRETXdAUU+IgOYPQNJF17haGWbeDxSyk++HA14UA98FurvPyBJOA== dependencies: chalk "^2.4.2" source-map "^0.6.1" @@ -9371,7 +9488,7 @@ readable-stream@1.0.27-1: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^3.0.6, readable-stream@^3.1.1: +readable-stream@^3.0.6, readable-stream@^3.1.1, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -9389,6 +9506,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@~3.4.0: + version "3.4.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.4.0.tgz#9fdccdf9e9155805449221ac645e8303ab5b9ada" + integrity sha512-0xe001vZBnJEK+uKcj8qOhyAKPzIT+gStxWr3LCB0DwcXR5NZJ3IaC+yGnHCYzB/S7ov3m3EEbZI2zeNvX+hGQ== + dependencies: + picomatch "^2.2.1" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -9494,9 +9618,9 @@ regexpu-core@^4.6.0, regexpu-core@^4.7.0: unicode-match-property-value-ecmascript "^1.2.0" regjsgen@^0.5.1: - version "0.5.1" - resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.1.tgz#48f0bf1a5ea205196929c0d9798b42d1ed98443c" - integrity sha512-5qxzGZjDs9w4tzT3TPhCJqWdCc3RLYwy9J2NB0nm5Lz+S273lvWcpjaTGHsT1dc6Hhfq41uSEOw8wBmxrKOuyg== + version "0.5.2" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.5.2.tgz#92ff295fb1deecbf6ecdab2543d207e91aa33733" + integrity sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A== regjsparser@^0.6.4: version "0.6.4" @@ -9581,11 +9705,6 @@ require-directory@^2.1.1: resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" integrity sha1-jGStX9MNqxyXbiNE/+f3kqam30I= -require-main-filename@^1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" - integrity sha1-l/cXtp1IeE9fUmpsWqj/3aBVpNE= - require-main-filename@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b" @@ -9654,9 +9773,9 @@ resolve@1.7.1: path-parse "^1.0.5" resolve@1.x, resolve@^1.1.7, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.3.2, resolve@^1.8.1: - version "1.16.1" - resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.16.1.tgz#49fac5d8bacf1fd53f200fa51247ae736175832c" - integrity sha512-rmAglCSqWWMrrBv/XM6sW0NuRFiKViw/W4d9EbC4pt+49H8JwHy+mcGmALTEg504AUDcLTvb1T2q3E9AnmY+ig== + version "1.17.0" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.17.0.tgz#b25941b54968231cc2d1bb76a79cb7f2c0bf8444" + integrity sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w== dependencies: path-parse "^1.0.6" @@ -9733,10 +9852,10 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: - version "5.2.0" - resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" - integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== +safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== safe-regex@^1.1.0: version "1.1.0" @@ -9770,15 +9889,15 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sass-graph@^2.2.4: - version "2.2.4" - resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.4.tgz#13fbd63cd1caf0908b9fd93476ad43a51d1e0b49" - integrity sha1-E/vWPNHK8JCLn9k0dq1DpR0eC0k= +sass-graph@2.2.5: + version "2.2.5" + resolved "https://registry.yarnpkg.com/sass-graph/-/sass-graph-2.2.5.tgz#a981c87446b8319d96dce0671e487879bd24c2e8" + integrity sha512-VFWDAHOe6mRuT4mZRd4eKE+d8Uedrk6Xnh7Sh9b4NGufQLQjOrvf/MQoOdx+0s92L89FeyUUNfU597j/3uNpag== dependencies: glob "^7.0.0" lodash "^4.0.0" scss-tokenizer "^0.2.3" - yargs "^7.0.0" + yargs "^13.3.2" sass-loader@^7.0.1: version "7.3.1" @@ -9837,7 +9956,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.6.1, schema-utils@^2.6.4, schema-utils@^2.6.5: +schema-utils@^2.6.1, schema-utils@^2.6.5, schema-utils@^2.6.6: version "2.6.6" resolved "https://registry.yarnpkg.com/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== @@ -9914,6 +10033,11 @@ serialize-javascript@^2.1.2: resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-2.1.2.tgz#ecec53b0e0317bdc95ef76ab7074b7384785fa61" integrity sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ== +serialize-javascript@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-3.0.0.tgz#492e489a2d77b7b804ad391a5f5d97870952548e" + integrity sha512-skZcHYw2vEX4bw90nAr2iTTsz6x2SrHEnfxgKYmZlvJYBEZrvbKtobJWlQ20zczKb3bsHHXXTYt48zBA7ni9cw== + serve-index@^1.9.1: version "1.9.1" resolved "https://registry.yarnpkg.com/serve-index/-/serve-index-1.9.1.tgz#d3768d69b1e7d82e5ce050fff5b453bea12a9239" @@ -10164,9 +10288,9 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: urix "^0.1.0" source-map-support@^0.5.6, source-map-support@~0.5.12: - version "0.5.18" - resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.18.tgz#f5f33489e270bd7f7d7e7b8debf283f3a4066960" - integrity sha512-9luZr/BZ2QeU6tO2uG8N2aZpVSli4TSAOAqFOyTO51AJcD9P99c0K1h6dD6r6qo5dyT44BR5exweOaLLeldTkQ== + version "0.5.19" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.19.tgz#a98b62f86dcaf4f67399648c085291ab9e8fed61" + integrity sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw== dependencies: buffer-from "^1.0.0" source-map "^0.6.0" @@ -10204,9 +10328,9 @@ spark-md5@^3.0.0: integrity sha512-0tF3AGSD1ppQeuffsLDIOWlKUd3lS92tFxcsrh5Pe3ZphhnoK+oXIBTzOAThZCiuINZLvpiLH/1VS1/ANEJVig== spdx-correct@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.0.tgz#fb83e504445268f154b074e218c87c003cd31df4" - integrity sha512-lr2EZCctC2BNR7j7WzJ2FpDznxky1sjfxvvYEyzxNyb6lZXHODmEoJeFu4JupYlkfha1KZpJyoqiJ7pgA1qq8Q== + version "3.1.1" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.1.1.tgz#dece81ac9c1e6713e5f7d1b6f17d468fa53d89a9" + integrity sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w== dependencies: spdx-expression-parse "^3.0.0" spdx-license-ids "^3.0.0" @@ -10217,9 +10341,9 @@ spdx-exceptions@^2.1.0: integrity sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A== spdx-expression-parse@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" - integrity sha512-Yg6D3XpRD4kkOmTpdgbUiEJFKghJH03fiC1OPll5h/0sO6neh2jqRDVHOQ4o/LMea0tgCkbMgea5ip/e+MkWyg== + version "3.0.1" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz#cf70f50482eefdc98e3ce0a6833e4a53ceeba679" + integrity sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q== dependencies: spdx-exceptions "^2.1.0" spdx-license-ids "^3.0.0" @@ -10379,7 +10503,7 @@ string-template@~0.2.0: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -string-width@^1.0.1, string-width@^1.0.2: +string-width@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-1.0.2.tgz#118bdf5b8cdc51a2a7e70d211e07e2b0b9b107d3" integrity sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M= @@ -10526,12 +10650,12 @@ style-loader@^0.21.0: schema-utils "^0.4.5" style-loader@^1.1.3: - version "1.1.4" - resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.1.4.tgz#1ad81283cefe51096756fd62697258edad933230" - integrity sha512-SbBHRD8fwK3pX+4UDF4ETxUF0+rCvk29LWTTI7Rt0cgsDjAj3SWM76ByTe6u2+4IlJ/WwluB7wuslWETCoPQdg== + version "1.2.1" + resolved "https://registry.yarnpkg.com/style-loader/-/style-loader-1.2.1.tgz#c5cbbfbf1170d076cfdd86e0109c5bba114baa1a" + integrity sha512-ByHSTQvHLkWE9Ir5+lGbVOXhxX10fbprhLvdg96wedFZb4NDekDPxVKv5Fwmio+QcMlkkNfuK+5W1peQ5CUhZg== dependencies: loader-utils "^2.0.0" - schema-utils "^2.6.5" + schema-utils "^2.6.6" stylehacks@^4.0.0: version "4.0.3" @@ -10664,24 +10788,24 @@ terser-webpack-plugin@^1.4.3: worker-farm "^1.7.0" terser-webpack-plugin@^2.3.5: - version "2.3.5" - resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.5.tgz#5ad971acce5c517440ba873ea4f09687de2f4a81" - integrity sha512-WlWksUoq+E4+JlJ+h+U+QUzXpcsMSSNXkDy9lBVkSqDn1w23Gg29L/ary9GeJVYCGiNJJX7LnVc4bwL1N3/g1w== + version "2.3.6" + resolved "https://registry.yarnpkg.com/terser-webpack-plugin/-/terser-webpack-plugin-2.3.6.tgz#a4014b311a61f87c6a1b217ef4f5a75bd0665a69" + integrity sha512-I8IDsQwZrqjdmOicNeE8L/MhwatAap3mUrtcAKJuilsemUNcX+Hier/eAzwStVqhlCxq0aG3ni9bK/0BESXkTg== dependencies: cacache "^13.0.1" - find-cache-dir "^3.2.0" - jest-worker "^25.1.0" - p-limit "^2.2.2" - schema-utils "^2.6.4" - serialize-javascript "^2.1.2" + find-cache-dir "^3.3.1" + jest-worker "^25.4.0" + p-limit "^2.3.0" + schema-utils "^2.6.6" + serialize-javascript "^3.0.0" source-map "^0.6.1" - terser "^4.4.3" + terser "^4.6.12" webpack-sources "^1.4.3" -terser@^4.1.2, terser@^4.4.3: - version "4.6.11" - resolved "https://registry.yarnpkg.com/terser/-/terser-4.6.11.tgz#12ff99fdd62a26de2a82f508515407eb6ccd8a9f" - integrity sha512-76Ynm7OXUG5xhOpblhytE7X58oeNSmC8xnNhjWVo8CksHit0U0kO4hfNbPrrYwowLWFgM2n9L176VNx2QaHmtA== +terser@^4.1.2, terser@^4.6.12: + version "4.7.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-4.7.0.tgz#15852cf1a08e3256a80428e865a2fa893ffba006" + integrity sha512-Lfb0RiZcjRDXCC3OSHJpEkxJ9Qeqs6mp2v4jf2MHfy8vGERmVDuvjXdd/EnP5Deme5F2yBRBymKmKHCBg2echw== dependencies: commander "^2.20.0" source-map "~0.6.1" @@ -10772,6 +10896,13 @@ to-regex-range@^2.1.0: is-number "^3.0.0" repeat-string "^1.6.1" +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + to-regex@^3.0.1, to-regex@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/to-regex/-/to-regex-3.0.2.tgz#13cfdd9b336552f30b51f33a8ae1b42a7a7599ce" @@ -10836,9 +10967,9 @@ ts-pnp@^1.1.6: integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== tslib@^1.9.0: - version "1.11.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.11.1.tgz#eb15d128827fbee2841549e171f45ed338ac7e35" - integrity sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== + version "1.13.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.13.0.tgz#c881e13cc7015894ed914862d276436fa9a47043" + integrity sha512-i/6DQjL8Xf3be4K/E6Wgpekn5Qasl1usyw++dAA35Ue5orEn65VIxOA+YvNNl9HV3qv70T7CNwjODHZrLwvd1Q== tty-browserify@0.0.0: version "0.0.0" @@ -10882,10 +11013,10 @@ typedarray@^0.0.6: resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.2: - version "3.8.3" - resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== +typescript@^3.9.3: + version "3.9.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" + integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== unicode-canonical-property-names-ecmascript@^1.0.4: version "1.0.4" @@ -11154,14 +11285,23 @@ walker@^1.0.7, walker@~1.0.5: dependencies: makeerror "1.0.x" -watchpack@^1.6.1: - version "1.6.1" - resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.6.1.tgz#280da0a8718592174010c078c7585a74cd8cd0e2" - integrity sha512-+IF9hfUFOrYOOaKyfaI7h7dquUIOgyEMoQMLA7OP5FxegKA2+XdXThAZ9TU2kucfhDH7rfMHs1oPYziVGWRnZA== +watchpack-chokidar2@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/watchpack-chokidar2/-/watchpack-chokidar2-2.0.0.tgz#9948a1866cbbd6cb824dea13a7ed691f6c8ddff0" + integrity sha512-9TyfOyN/zLUbA288wZ8IsMZ+6cbzvsNyEzSBp6e/zkifi6xxbl8SmQ/CxQq32k8NNqrdVEVUVSEf56L4rQ/ZxA== dependencies: chokidar "^2.1.8" + +watchpack@^1.6.1: + version "1.7.2" + resolved "https://registry.yarnpkg.com/watchpack/-/watchpack-1.7.2.tgz#c02e4d4d49913c3e7e122c3325365af9d331e9aa" + integrity sha512-ymVbbQP40MFTp+cNMvpyBpBtygHnPzPkHqoIwRRj/0B8KhqQwV8LaKjtbaxF2lK4vl8zN9wCxS46IFCU5K4W0g== + dependencies: graceful-fs "^4.1.2" neo-async "^2.5.0" + optionalDependencies: + chokidar "^3.4.0" + watchpack-chokidar2 "^2.0.0" wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" @@ -11308,11 +11448,11 @@ websocket-driver@0.6.5: websocket-extensions ">=0.1.1" websocket-driver@>=0.5.1: - version "0.7.3" - resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" - integrity sha512-bpxWlvbbB459Mlipc5GBzzZwhoZgGEZLuqPaR0INBGnPAY1vdBX6hPnoFXiw+3yWxDuHyQjO2oXTMyS8A5haFg== + version "0.7.4" + resolved "https://registry.yarnpkg.com/websocket-driver/-/websocket-driver-0.7.4.tgz#89ad5295bbf64b480abcba31e4953aca706f5760" + integrity sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg== dependencies: - http-parser-js ">=0.4.0 <0.4.11" + http-parser-js ">=0.5.1" safe-buffer ">=5.1.0" websocket-extensions ">=0.1.1" @@ -11356,11 +11496,6 @@ whet.extend@~0.9.9: resolved "https://registry.yarnpkg.com/whet.extend/-/whet.extend-0.9.9.tgz#f877d5bf648c97e5aa542fadc16d6a259b9c11a1" integrity sha1-+HfVv2SMl+WqVC+twW1qJZucEaE= -which-module@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" - integrity sha1-u6Y8qGGUiZT/MHc2CJ47lgJsKk8= - which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" @@ -11392,13 +11527,12 @@ worker-farm@^1.7.0: dependencies: errno "~0.1.7" -wrap-ansi@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85" - integrity sha1-2Pw9KE3QV5T+hJc8rs3Rz4JP3YU= +worker-rpc@^0.1.0: + version "0.1.1" + resolved "https://registry.yarnpkg.com/worker-rpc/-/worker-rpc-0.1.1.tgz#cb565bd6d7071a8f16660686051e969ad32f54d5" + integrity sha512-P1WjMrUB3qgJNI9jfmpZ/htmBEjFh//6l/5y8SD9hg1Ef5zTTVVoRjTrTEzPrNBQvmhMxkoTsjOXN10GWU7aCg== dependencies: - string-width "^1.0.1" - strip-ansi "^3.0.1" + microevent.ts "~0.1.1" wrap-ansi@^5.1.0: version "5.1.0" @@ -11470,11 +11604,6 @@ xtend@^4.0.0, xtend@^4.0.1, xtend@~4.0.0, xtend@~4.0.1: resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== -y18n@^3.2.1: - version "3.2.1" - resolved "https://registry.yarnpkg.com/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" - integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= - y18n@^4.0.0: version "4.0.0" resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" @@ -11496,11 +11625,9 @@ yallist@^4.0.0: integrity sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== yaml@^1.7.2: - version "1.9.2" - resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.9.2.tgz#f0cfa865f003ab707663e4f04b3956957ea564ed" - integrity sha512-HPT7cGGI0DuRcsO51qC1j9O16Dh1mZ2bnXwsi0jrSpsLz0WxOLSLXfkABVl6bZO629py3CU+OMJtpNHDLB97kg== - dependencies: - "@babel/runtime" "^7.9.2" + version "1.10.0" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" + integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== yargs-parser@10.x: version "10.1.0" @@ -11517,13 +11644,6 @@ yargs-parser@^13.1.0, yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-5.0.0.tgz#275ecf0d7ffe05c77e64e7c86e4cd94bf0e1228a" - integrity sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo= - dependencies: - camelcase "^3.0.0" - yargs@13.2.4: version "13.2.4" resolved "https://registry.yarnpkg.com/yargs/-/yargs-13.2.4.tgz#0b562b794016eb9651b98bd37acf364aa5d6dc83" @@ -11557,25 +11677,6 @@ yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^7.0.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/yargs/-/yargs-7.1.0.tgz#6ba318eb16961727f5d284f8ea003e8d6154d0c8" - integrity sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg= - dependencies: - camelcase "^3.0.0" - cliui "^3.2.0" - decamelize "^1.1.1" - get-caller-file "^1.0.1" - os-locale "^1.4.0" - read-pkg-up "^1.0.1" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^1.0.2" - which-module "^1.0.0" - y18n "^3.2.1" - yargs-parser "^5.0.0" - yauzl@^2.10.0: version "2.10.0" resolved "https://registry.yarnpkg.com/yauzl/-/yauzl-2.10.0.tgz#c7eb17c93e112cb1086fa6d8e51fb0667b79a5f9" From 0f709994940ac3c0cee64fb4771e8befe9a2be1a Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 17:30:41 -0500 Subject: [PATCH 423/440] Rename doc files to be consistent --- docs/{GETTING_STARTED.MD => getting_started.md} | 0 docs/{KNOWN_ISSUES.MD => known_issues.md} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/{GETTING_STARTED.MD => getting_started.md} (100%) rename docs/{KNOWN_ISSUES.MD => known_issues.md} (100%) diff --git a/docs/GETTING_STARTED.MD b/docs/getting_started.md similarity index 100% rename from docs/GETTING_STARTED.MD rename to docs/getting_started.md diff --git a/docs/KNOWN_ISSUES.MD b/docs/known_issues.md similarity index 100% rename from docs/KNOWN_ISSUES.MD rename to docs/known_issues.md From e5fa2d0b554e99abfcaaaf08804b35e7c952d32d Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 27 May 2020 17:31:36 -0500 Subject: [PATCH 424/440] Move Docker set up to its own file --- docs/docker.md | 69 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 docs/docker.md diff --git a/docs/docker.md b/docs/docker.md new file mode 100644 index 00000000..1d1a0095 --- /dev/null +++ b/docs/docker.md @@ -0,0 +1,69 @@ +#### Docker install (if you don't have docker and docker-compose installed) +##### install Docker and Docker compose +You need to install Docker and Docker Compose. +* *Note:* Docker and Docker Compose binaries from Docker itself are proprietary software based entirely upon +free software. If you feel more comfortable, you may build them from source. + +* *Note 2:* For Debian, the Docker package is simply too out of date to be usable. +Even the version for latest Ubuntu LTS is too old. For reliability, we strongly +recommend using the Docker debian feed from docker itself OR making sure you keep your +own build up to date. + +##### Add yourself to the docker group +Adding yourself as a Docker group user as follows: + +`sudo usermod -aG docker $USER` + +You will likely need to logout and log back in again. + +#### Build your docker-container and start it up for initial set up. +We'll keep this running in the console we'll call **console 1** +``` +./dc build +./dc up +``` +#### System configuration +There are a number of steps for configuring your Houdini instance for startup +##### Start a new console we'll call **console 2**. + +##### In console 2, copy the env template to your .env file + ``` + cp .env.template .env + ``` +##### In console 2, run the following and copy the output to you .env file to set you `DEVISE_SECRET_KEY` environment variable. +`./run rake secret # copy this result into your DEVISE_SECRET_KEY` + +##### In console 2, , run the following and copy the output to you .env file to set you `SECRET_TOKEN` environment variable. +``` +./run rake secret # copy this result into your SECRET_TOKEN +``` + +##### Set the following secrets in your .env file with your Stripe account information +- `STRIPE_API_KEY` with your Stripe PRIVATE key +- `STRIPE_API_PUBLIC` with your Stripe PUBLIC key + +##### You SHOULD set your AMAZON s3 information (optional but STRONGLY recommended) +If you don't, file uploads WILL NOT WORK but it's not required. + +##### In console 2, install yarn +`./run yarn` + +##### In console 2, fill the db +`./run rake db:create db:structure:load db:seed test:prepare` + +##### Set up mailer info +You can set this in `config/default_organization.yml` or better yet, make a copy with your own org name and add that to your .env file as `ORG_NAME` +If you need help setting up your mailer, visit `config/environment.rb` where the settings schema is verified and documented. + +#### Startup +##### Switch back to console 1 and run `Ctrl-c` to end the session. + +##### In console 1, restart the containers +`./dc up` + +##### In console 2, run: +`./run yarn watch` + +##### You can go to http://localhost:5000 + +To get started, register your nonprofit using the "Get Started" link. \ No newline at end of file From 760dc7e99b70dbc85588fa1ab2f6aa9b4ba7d549 Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 29 May 2020 15:33:29 -0500 Subject: [PATCH 425/440] Correct bug in creating portions of the app_data --- app/views/app_data/_user.jbuilder | 2 +- app/views/layouts/_app_data.html.erb | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/app_data/_user.jbuilder b/app/views/app_data/_user.jbuilder index 7637a854..a492df98 100644 --- a/app/views/app_data/_user.jbuilder +++ b/app/views/app_data/_user.jbuilder @@ -5,4 +5,4 @@ json.extract! user, :id, :created_at, :updated_at, :email json.unconfirmed_email user.unconfirmed_email json.confirmed user.confirmed? -json.partial! 'app_data/profile.json', profile: user.profile +json.partial! 'app_data/profile', profile: user.profile diff --git a/app/views/layouts/_app_data.html.erb b/app/views/layouts/_app_data.html.erb index a7b3da62..8fd3fb5e 100644 --- a/app/views/layouts/_app_data.html.erb +++ b/app/views/layouts/_app_data.html.erb @@ -7,9 +7,9 @@ var app = { , current_admin: <%= !!(current_user && current_role?(:super_admin)) %> , nonprofit: <%= @nonprofit ? raw(render('app_data/nonprofit', nonprofit: @nonprofit)) : 'undefined' %> , nonprofit_id : <%= @nonprofit ? @nonprofit.id : 'undefined' %> -, user: <%= current_user ? raw(render('app_data/user.json', user: current_user)) : 'undefined' %> +, user: <%= current_user ? raw(render('app_data/user', user: current_user)) : 'undefined' %> , user_id: <%= current_user ? current_user.id : 'undefined' %> -, profile: <%= current_user&.profile ? raw(render('app_data/profile.json', profile: current_user.profile)) : 'undefined' %> +, profile: <%= current_user&.profile ? raw(render('app_data/profile', profile: current_user.profile)) : 'undefined' %> , profile_id: <%= current_user&.profile ? current_user.profile.id : 'undefined' %> , asset_path: "<%= Rails.application.config.assets.prefix %>" , host_with_port: "//<%= request.host_with_port %>" From 24bcef23c637ed16db8481bd21b0078736016ceb Mon Sep 17 00:00:00 2001 From: Eric Date: Fri, 29 May 2020 16:54:53 -0500 Subject: [PATCH 426/440] Migrate from imagemagick to vips for image transformation --- Gemfile | 1 - Gemfile.lock | 1 - app/models/concerns/image/attachment_extensions.rb | 2 +- config/application.rb | 2 ++ 4 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 612c0cc6..08e371ae 100755 --- a/Gemfile +++ b/Gemfile @@ -52,7 +52,6 @@ gem 'param_validation', path: 'gems/ruby-param-validation' gem 'qx', path: 'gems/ruby-qx' # Images -gem 'mini_magick', '~> 4.10.1' gem 'image_processing', '~> 1.10.3' # User authentication diff --git a/Gemfile.lock b/Gemfile.lock index 1a850739..a51e8511 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -486,7 +486,6 @@ DEPENDENCIES listen lograge (~> 0.11.2) mail_view (~> 2.0) - mini_magick (~> 4.10.1) nearest_time_zone (~> 0.0.4) parallel (~> 1.17) parallel_tests (~> 2.32) diff --git a/app/models/concerns/image/attachment_extensions.rb b/app/models/concerns/image/attachment_extensions.rb index 4d10c0d4..296c29c0 100644 --- a/app/models/concerns/image/attachment_extensions.rb +++ b/app/models/concerns/image/attachment_extensions.rb @@ -27,7 +27,7 @@ module Image::AttachmentExtensions #{sizes.map do |k,v| <<-INNER when :#{k.to_sym} - return #{attribute}.variant(resize: "#{v[0]}x#{v[1]}") + return #{attribute}.variant(resize_to_limit: [#{v[0]}, #{v[1]}]) INNER end.join("\n")} else diff --git a/config/application.rb b/config/application.rb index ee4c5499..c8af71bd 100755 --- a/config/application.rb +++ b/config/application.rb @@ -108,5 +108,7 @@ module Commitchange # we don't require belongs_to associations to be required for historical reasons. config.active_record.belongs_to_required_by_default = false + + config.active_storage.variant_processor = :vips end end From fe69c60be80107a7e6406a03745a1a92a2040e03 Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 1 Jun 2020 14:01:33 -0500 Subject: [PATCH 427/440] AWS is no longer required so we make it optional in setup --- config/environment.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/environment.rb b/config/environment.rb index e636496e..d7e3395a 100755 --- a/config/environment.rb +++ b/config/environment.rb @@ -57,7 +57,7 @@ Config.schema do end end - required(:aws).schema do + optional(:aws).schema do # the region your AWS bucket is in optional(:region).filled(:str?) From 1c49a696e94353a4f6f937a8eda1148957efe650 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 11:12:00 -0500 Subject: [PATCH 428/440] Fix merge bug in English locale info --- config/locales/en.yml | 66 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/config/locales/en.yml b/config/locales/en.yml index bd8814c5..59e81e51 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -1,3 +1,4 @@ +# License: CC0-1.0 # Files in the config/locales directory are used for internationalization # and are automatically loaded by Rails. If you want to use locales other # than English, add the necessary files in this directory. @@ -37,6 +38,8 @@ en: body: 'Comment content' organization: name: "Organisation" + organization_page: + promote: "Promote this organization" donation: amount: "Total Amount" date: "Transaction Date" @@ -54,8 +57,9 @@ en: subject: "Donation receipt for %{nonprofit_name}" transfer_info_html: "This transfer will appear on your bank statement as %{label}" transfer_label_html: "Donation %{nonprofit_statement}." - oneoff_donation_html: "Thank you for your donation to %{nonprofit_name} and for joining thousands of people across Europe who are invested in making our movement a true force to be reckoned with. Your support will go towards ensuring we can move fast to win the campaigns that matter to all of us." - recurring_donation_html: "Thank you for your regular donation to %{nonprofit_name} and for joining thousands of people across Europe who are invested in making our movement a true force to be reckoned with. Your support will go towards ensuring we can move fast to win the campaigns that matter to all of us." + oneoff_donation_html: "Your donation towards %{nonprofit_name} was successful!" + recurring_donation_html: "Your recurring donation towards %{nonprofit_name}, started on %{start_date}, has been successfully paid." + recurring_donation_cancel_modify_html: "If you need to update your card or cancel your recurring donation, please follow this link: %{management_url}" donor_direct_debit_notification: subject: "Donation receipt for %{nonprofit_name}" transfer_info_html: "This transfer will appear on your bank statement as %{label}" @@ -166,3 +170,61 @@ en: twitter: "Tweet" twitter_message: "Join me in supporting" finish: "Finish" + registration: + get_started: + header: "Get started" + description: "Let's get started with Houdini. To begin, fill out your initial nonprofit and info." + wizard: + tabs: + nonprofit: "Nonprofit" + contact: "Contact" + nonprofit: + name: + label: "Organization Name" + placeholder: "Ending Poverty in the Fox Valley Inc." + website: + label: "Website URL" + placeholder: "http://www.endpovertyinthefoxvalleyinc.org" + email: + label: "Org Email (public)" + placeholder: "contact@endpovertyinthefoxvalleyinc.org" + phone: + label: "Org Phone (public)" + placeholder: "(555) 555-5555" + city: + label: "City" + placeholder: "Appleton" + state: + label: "State" + placeholder: "WI" + zip: + label: "Zip Code" + placeholder: "54915" + contact: + name: + label: "Your Name" + placeholder: "Penelope Schultz" + email: + label: "Your Email (used for login)" + placeholder: "penelope@endpovertyinthefoxvalleyinc.org" + password: + label: "New Password" + password_confirmation: + label: "Retype Password" + phone: + label: "Your Phone (for account recovery)" + placeholder: "(555) 555-5555" + save_and_finish: "Save & Finish" + saving: "Saving..." + next: "Next" + footer: + terms_and_privacy: "Terms & Privacy" + about: "About" + login: + header: "Login" + email: "Email" + password: "Password" + login: "Login" + logging_in: "Logging you in..." + forgot_password: "Forgot Password?" + get_started: "Get Started" \ No newline at end of file From 28e0e373dfd10d5c3ea2d4117e960fa423757235 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 15:50:18 -0500 Subject: [PATCH 429/440] Set default profile pic --- app/models/profile.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/models/profile.rb b/app/models/profile.rb index a8ab05b8..538205c8 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -54,6 +54,8 @@ class Profile < ApplicationRecord self.name ||= user.name if user self.email ||= user.email if user self.picture ||= user.picture if user + picture.attach(io: File.open(Settings.default.image.profile), + filename: "profile-image.png") unless self.picture.attached? if self.name.blank? && first_name.present? && last_name.present? self.name ||= first_name + ' ' + last_name end From 354b93bbf9b7cf266e4ce83dab576d48c74f6598 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 16:35:43 -0500 Subject: [PATCH 430/440] Fix setting the profile pic from user pic on creation --- app/models/profile.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index 538205c8..afe1d5fd 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -53,7 +53,7 @@ class Profile < ApplicationRecord def set_defaults self.name ||= user.name if user self.email ||= user.email if user - self.picture ||= user.picture if user + self.picture.attach(user.picture) if user && user.picture.attached? picture.attach(io: File.open(Settings.default.image.profile), filename: "profile-image.png") unless self.picture.attached? if self.name.blank? && first_name.present? && last_name.present? From 6028b64cd73af2f3869c59569b05b166075ae93e Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 17:01:46 -0500 Subject: [PATCH 431/440] Remove picture column from profile --- db/structure.sql | 3 --- .../migrate/20200602215911_remove_picture_from_profile.rb | 8 ++++++++ 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 gems/houdini_upgrade/db/migrate/20200602215911_remove_picture_from_profile.rb diff --git a/db/structure.sql b/db/structure.sql index bd0d7e3e..7a350c1f 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -95,8 +95,6 @@ WHERE (supporters.id=NEW.supporter_id)) AS data SET default_tablespace = ''; -SET default_with_oids = false; - -- -- Name: active_storage_attachments; Type: TABLE; Schema: public; Owner: - -- @@ -1691,7 +1689,6 @@ CREATE TABLE public.profiles ( state_code character varying(255), city character varying(255), privacy_settings text, - picture character varying(255), phone character varying(255), address character varying(255), anonymous boolean, diff --git a/gems/houdini_upgrade/db/migrate/20200602215911_remove_picture_from_profile.rb b/gems/houdini_upgrade/db/migrate/20200602215911_remove_picture_from_profile.rb new file mode 100644 index 00000000..b40f03bf --- /dev/null +++ b/gems/houdini_upgrade/db/migrate/20200602215911_remove_picture_from_profile.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class RemovePictureFromProfile < ActiveRecord::Migration[6.0] + def change + remove_column :profiles, :picture + end +end From 7a92df977bf98039d190879c53ecf1e681682cf2 Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 17:12:06 -0500 Subject: [PATCH 432/440] bin/setup now installs JS, copies .env and creates secrets --- bin/setup | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/bin/setup b/bin/setup index 0e39e8cb..39fcd7c5 100755 --- a/bin/setup +++ b/bin/setup @@ -1,5 +1,6 @@ #!/usr/bin/env ruby require 'fileutils' +require 'securerandom' # path to your application root. APP_ROOT = File.expand_path('..', __dir__) @@ -13,14 +14,20 @@ FileUtils.chdir APP_ROOT do # This script is idempotent, so that you can run it at anytime and get an expectable outcome. # Add necessary setup steps to this file. - puts '== Installing dependencies ==' + puts '== Installing dependencies (Ruby) ==' system! 'gem install bundler --conservative' system('bundle check') || system!('bundle install') - # puts "\n== Copying sample files ==" - # unless File.exist?('config/database.yml') - # FileUtils.cp 'config/database.yml.sample', 'config/database.yml' - # end + puts "\n== Installing dependencies (JS) ==" + system!('yarn -s') + + puts "\n== Copying env file ==" + unless File.exist?('.env') + FileUtils.cp '.env.template', '.env' + end + + puts "\n== Create new secrets ==" + File.write('.env', File.read('.env').gsub(/-- secret string --/) { SecureRandom.hex(64) }) puts "\n== Preparing database ==" system! 'bin/rails db:prepare' From 342959887486c0f7b6aeba60ea56f688614a32dc Mon Sep 17 00:00:00 2001 From: Eric Date: Tue, 2 Jun 2020 17:12:53 -0500 Subject: [PATCH 433/440] Remove unneeded profile picture assign --- app/models/profile.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/models/profile.rb b/app/models/profile.rb index afe1d5fd..e53d5d3a 100755 --- a/app/models/profile.rb +++ b/app/models/profile.rb @@ -53,7 +53,6 @@ class Profile < ApplicationRecord def set_defaults self.name ||= user.name if user self.email ||= user.email if user - self.picture.attach(user.picture) if user && user.picture.attached? picture.attach(io: File.open(Settings.default.image.profile), filename: "profile-image.png") unless self.picture.attached? if self.name.blank? && first_name.present? && last_name.present? From 4a240c1d51659d30d0eed36e1a18aac6f348d95f Mon Sep 17 00:00:00 2001 From: Eric Date: Mon, 1 Jun 2020 14:14:04 -0500 Subject: [PATCH 434/440] Update README.md for Rails v6 --- .env.template | 2 +- README.md | 156 ++++++++++++++++++++++++++++---------------------- 2 files changed, 87 insertions(+), 71 deletions(-) diff --git a/.env.template b/.env.template index b6128da6..98a5b349 100644 --- a/.env.template +++ b/.env.template @@ -24,7 +24,7 @@ export MAILCHIMP_REDIRECT_URL='REPLACE' export FACEBOOK_APP_ID="REPLACE" -export CYPHER_KEY="REPLACE" # used for mailchimp integration +export CYPHER_KEY="-- secret string --" # used for mailchimp integration diff --git a/README.md b/README.md index d9ac0bb5..939c5631 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,14 @@ comfort and speed. All backend code and React components should be TDD. +## Prerequisites +Houdini is designed and tested to run with the following: +* Ruby 2.6 +* Node 12 +* Yarn +* PostgreSQL 11 +* Ubuntu 20.04 or equivalent + ## Get involved Houdini's success depends on you! @@ -28,90 +36,98 @@ https://houdini.zulipchat.com ### Help with translations Visit the Internationalization channel on Houdini Zulip and discuss + ## Dev Setup +#### Tips for specific circumstances +* Docker: Docker was previously used for development of Houdini. +See [docker.md](docs/docker.md) for more info. +* Mac: Mac dev setup may require some unique configuration. +See [mac_getting_started.md](docs/mac_getting_started.md) for more info. -### Create new postgres user -Run `sudo -u postgres createuser houdini_user -s -P` and then enter a password for the role +### Installation prep +Houdini requires a few pieces of software be installed, as well as some optional pieces +which make development much easier. -## Docker setup +These include: + +* PostgreSQL 12 +* NodeJS 12 LTS +* Ruby 2.6.2 (NOTE: the default of Ruby 2.7.1 in Debian will likely function but you will receive a ton of deprecation warnings from Ruby) +* RVM (optional, simplifies managing multiple ruby versions) + +#### One-time setup + +You'll want to run the next commands as root or via sudo. You could do this by typing `sudo /bin/sh` running the commands from there. + +TIP: this is the root shell. There's no restrictions on what you do here so be careful! +```bash +apt update +apt install curl -yy +curl -sL https://deb.nodesource.com/setup_12.x | bash - +curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - +echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list +apt update +apt install git postgresql-12 libpq-dev libjemalloc-dev libvips42 yarn -yy +``` + +You'll run the next commands as your normal user. + +NOTE: in the case of a production instance, this might be +your web server's user. + +NOTE 2: We use [RVM](https://rvm.io) to have more control over the exact version of Ruby. For development, it's also way easier because you can +use a consistent version of Ruby (and different sets of installed gems) for different projects. You could also use rbenv +or simply build ruby from source. + +NOTE 3: We don't recommend using Ruby 2.7, the current Ubuntu default at this time. Ruby 2.7 will function but spits out tons +of deprecation warnings when using Rails applications. + +TIP: To get out of the root shell, run `exit` + +```bash +# add rvm keys +curl -sSL https://rvm.io/mpapis.asc | gpg --import - +curl -sSL https://rvm.io/pkuczynski.asc | gpg --import - +curl -sSL https://get.rvm.io | bash -s stable +source $HOME/.rvm/scripts/rvm +echo 'source "$HOME/.rvm/scripts/rvm"' >> ~/.bashrc +rvm install 2.6.6 --disable-binary --with-jemalloc +``` + + Run the following command as the `postgres` user and then enter your houdini_user + password at the prompt. + +NOTE: For development, Houdini expects the password to be 'password'. This would be terrible +for production but for development, it's likely not a huge issue. + +TIP: To run this, add `sudo -u postgres ` to the beginning of the following command. + +`createuser houdini_user -s -d -P` + +Now that we have all of our prerequisites prepared, we need to get the Houdini code. -#### Get the code `git clone https://github.com/HoudiniProject/houdini` -#### Mac Setup -If you have a Mac and don't want to run the `docker` configuration, see [how to get started](docs/GETTING_STARTED.MD) with the project. +This will download the latest Houdini code. Change to the +`houdini` directory and we can set the rest of Houdini up. -#### Docker install (if you don't have docker and docker-compose installed) -##### install Docker and Docker compose -You need to install Docker and Docker Compose. -* *Note:* Docker and Docker Compose binaries from Docker itself are proprietary software based entirely upon -free software. If you feel more comfortable, you may build them from source. +Let's run the Houdini project setup and we'll be ready to go! -* *Note 2:* For Debian, the Docker package is simply too out of date to be usable. -Even the version for latest Ubuntu LTS is too old. For reliability, we strongly -recommend using the Docker debian feed from docker itself OR making sure you keep your -own build up to date. - -##### Add yourself to the docker group -Adding yourself as a Docker group user as follows: - -`sudo usermod -aG docker $USER` - -You will likely need to logout and log back in again. - -#### Build your docker-container and start it up for initial set up. -We'll keep this running in the console we'll call **console 1** -``` -./dc build -./dc up -``` -#### System configuration -There are a number of steps for configuring your Houdini instance for startup -##### Start a new console we'll call **console 2**. - -##### In console 2, copy the env template to your .env file - ``` - cp .env.template .env - ``` -##### In console 2, run the following and copy the output to you .env file to set you `DEVISE_SECRET_KEY` environment variable. -`./run rake secret # copy this result into your DEVISE_SECRET_KEY` - -##### In console 2, , run the following and copy the output to you .env file to set you `SECRET_TOKEN` environment variable. -``` -./run rake secret # copy this result into your SECRET_TOKEN +```bash +bin/setup ``` -##### Set the following secrets in your .env file with your Stripe account information -- `STRIPE_API_KEY` with your Stripe PRIVATE key -- `STRIPE_API_PUBLIC` with your Stripe PUBLIC key +NOTE: The .env file holds your environment variables for development; on production you might +have these set somewhere else other than this file. -##### You SHOULD set your AMAZON s3 information (optional but STRONGLY recommended) -If you don't, file uploads WILL NOT WORK but it's not required. +TIP: On Heroku, the environment variables are set in your Dashboard. -##### In console 2, install yarn -`./run yarn` - -##### In console 2, fill the db -`./run rake db:create db:structure:load db:seed test:prepare` - -##### Set up mailer info -You can set this in `config/default_organization.yml` or better yet, make a copy with your own org name and add that to your .env file as `ORG_NAME` -If you need help setting up your mailer, visit `config/environment.rb` where the settings schema is verified and documented. +Also, you should set the STRIPE_API_KEY and STRIPE_API_PUBLIC environment variables which you'd get from the Stripe dashboard. On your development environment, make sure to use test keys. If you don't, you're +going to be charged real money! #### Startup -##### Switch back to console 1 and run `Ctrl-c` to end the session. - -##### In console 1, restart the containers -`./dc up` - -##### In console 2, run: -`./run yarn watch` - -##### You can go to http://localhost:5000 - -To get started, register your nonprofit using the "Get Started" link. - -## Additional info +`bin/rails server` +You can connect to your server at http://localhost:5000 ##### Super admin There is a way to set your user as a super_admin. This role lets you access any of the nonprofits From 32f8fe1cb25edc98f6b4115516ae818108d5b0f3 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 3 Jun 2020 11:35:39 -0500 Subject: [PATCH 435/440] Update README.md --- README.md | 107 ++++++++++++++++++++++++------------------------------ 1 file changed, 48 insertions(+), 59 deletions(-) diff --git a/README.md b/README.md index 939c5631..5cd868aa 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ [![](https://img.shields.io/badge/zulip-join_chat-brightgreen.svg)](https://houdini.zulipchat.com) [![Build Status](https://travis-ci.com/houdiniproject/houdini.svg?branch=master)](https://travis-ci.com/houdiniproject/houdini) +* NOTE: This is the latest version (pre-2.0) of Houdini and +is currently in HEAVY development. You may want +to use +[v1](https://github.com/houdiniproject/houdini/tree/1-0-stable) +instead. + + The Houdini Project is free and open source fundraising infrastructure. It includes... - Crowdfunding campaigns - Donate widget page and generator @@ -12,13 +19,11 @@ The Houdini Project is free and open source fundraising infrastructure. It inclu - Nonprofit org user account management - Simple donation management for donors -Much of the business logic is in `/lib`. - The frontend is written in a few custom frameworks, the largest of which is called Flimflam. We endeavor to migrate to React as quickly as possible to increase development comfort and speed. -All backend code and React components should be TDD. +All new backend code and React components well tested. ## Prerequisites Houdini is designed and tested to run with the following: @@ -50,10 +55,24 @@ which make development much easier. These include: -* PostgreSQL 12 +* PostgreSQL 11 * NodeJS 12 LTS -* Ruby 2.6.2 (NOTE: the default of Ruby 2.7.1 in Debian will likely function but you will receive a ton of deprecation warnings from Ruby) -* RVM (optional, simplifies managing multiple ruby versions) +* Ruby 2.6.6 (NOTE: the default of Ruby 2.7.1 in Debian should +function but you will receive a ton of deprecation +warnings from Ruby) + +There a few optional tools which make working on Houdini +easiter +* Ruby Version Manager (RVM) - RVM makes it simple to switch +between versions of Ruby for different projects. Additionally, you can +use different "gemsets" per version so you can separate the +state of a set of different projects. It will also switch +versions at the console when you change to a directory for +an project prepared for RVM, like Houdini. +* Automatic Version Switching for Node (AVN) - similar to RVM, AVN makes it simple to switch between versions of Node. When +properly configured, it automatically switches version at +the console whe you change to a directory for a project +prepared for AVN, like Houdini. #### One-time setup @@ -67,7 +86,7 @@ curl -sL https://deb.nodesource.com/setup_12.x | bash - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add - echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list apt update -apt install git postgresql-12 libpq-dev libjemalloc-dev libvips42 yarn -yy +apt install git postgresql-11 libpq-dev libjemalloc-dev libvips42 yarn -yy ``` You'll run the next commands as your normal user. @@ -82,6 +101,10 @@ or simply build ruby from source. NOTE 3: We don't recommend using Ruby 2.7, the current Ubuntu default at this time. Ruby 2.7 will function but spits out tons of deprecation warnings when using Rails applications. +NOTE 4: We recommend building Ruby with jemalloc support as we +do in these instructions. In practice, it manages memory far +more efficiently in Rails-based projects. + TIP: To get out of the root shell, run `exit` ```bash @@ -122,7 +145,10 @@ have these set somewhere else other than this file. TIP: On Heroku, the environment variables are set in your Dashboard. -Also, you should set the STRIPE_API_KEY and STRIPE_API_PUBLIC environment variables which you'd get from the Stripe dashboard. On your development environment, make sure to use test keys. If you don't, you're +Also, you should set the STRIPE_API_KEY and STRIPE_API_PUBLIC +environment variables which you'd get from the Stripe +dashboard. On your development environment, +make sure to use test keys. If you don't, you're going to be charged real money! #### Startup @@ -136,7 +162,7 @@ nonprofits, which is located at `/admin` url. To create the super user, go to the rails console by calling: -`./dc run web rails console` +`bin/rails console` In the console, run the following: @@ -149,36 +175,19 @@ role=Role.create(user:admin,name: "super_admin") For a list of [how to solve known issues](docs/KNOWN_ISSUES.MD) -## To run in production +## Run in production +You will likely want to make a few changes in your configuration of Houdini before running in production as you +would for any Rails project. These include: -##### Docker -While Docker should be very possible to use for production, the current Docker solution -is optimized heavily for dev purposes. If you know more about creating a solid production Docker setup, please do -contribute! - -(To be continued) -- rake assets:precompile -- if production: make sure memcached is running. - - -## Frontend - -Assets get compiled from `/client` to `/public/client` - -## React Generators -If creating new React or Typescript code, please use the Rails generators with the 'react:' prefix. This include: - -### react:packroot -This generator creates a new entry for Webpack. This is a place where Webpack will start -when packing a new javascript output file. It also creates a corresponding component for the entry. -Usually, you will have one of these per page. - -### react:component -This generator creates a React component along with a test file for testing with Jest. -Each component should have its own file. - -### react:lib -This generator creates a basic Typescript module along with a test file. +* Using a [different ActiveJob backend](https://guides.rubyonrails.org/active_job_basics.html). NOTE: The Sneakers for RabbitMQ doesn't +work properly. There are +[forks of Sneakers](https://github.com/veeqo/advanced-sneakers-activejob) +which might work but they haven't been tested. **If you do test +them please let us know!** +* Use a [proper cache store](https://guides.rubyonrails.org/caching_with_rails.html#cache-stores). The development uses + `memory_store` which isn't shared between processes or server + and clears every time your server software restarts. Memcached + or Redis are good choices here. ### Providing the complete corresponding source code @@ -195,24 +204,4 @@ For this to work though, the following characteristics must be true: * Your have to have committed any changes you made to the project in `HEAD` in your git repository * The `.git` folder for your repository must be a direct subfolder of your `$RAILS_ROOT` -* Your web server must be able to run `git archive`. - - -### Style - -#### Ruby -- 2 spaces for tabs - -#### New frontend code -- All new front end code should be written in Typescript -and React (using TSX files). Please use the React Generators for creation. -- 2 spaces for tabs - -#### Legacy Javascript -- 2 spaces for tabs -- Comma-led lines -- ES6 imports - -#### Git - -- No need to rebase, just merge +* Your web server must be able to run `git archive`. \ No newline at end of file From b27aa8e72459e726ad315c77f5d5887c2cd2f050 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 4 Jun 2020 10:00:25 -0500 Subject: [PATCH 436/440] Update year in validateCardExpiry test --- .../legacy_react/src/lib/payments/credit_card.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts b/app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts index 8edfcabf..29bb2bc4 100644 --- a/app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts +++ b/app/javascript/legacy_react/src/lib/payments/credit_card.spec.ts @@ -191,7 +191,7 @@ describe('CreditCardTypeManager', () => { }) it('should support year shorthand', () => { - expect(cc.validateCardExpiry('05', '20')).toBeTruthy() + expect(cc.validateCardExpiry('05', '25')).toBeTruthy() }) }) describe('Validating a CVC number', () => { From 23ef8c6d7cc5ab61cd1090685b58781efb7c85e3 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 4 Jun 2020 10:28:37 -0500 Subject: [PATCH 437/440] Ignore vendor patterns in jest tests --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index f9cc7fec..f77cac88 100644 --- a/package.json +++ b/package.json @@ -154,7 +154,8 @@ }, "testPathIgnorePatterns": [ "/node_modules/", - "/config/webpack/test.js" + "/config/webpack/test.js", + "/vendor/" ], "testRegex": "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", "moduleFileExtensions": [ From 03b0f4279e606da44603ffc5883cc92a6f4a3a12 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 3 Jun 2020 16:37:56 -0500 Subject: [PATCH 438/440] Correct rails-i18n Gem install command --- Gemfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile b/Gemfile index 08e371ae..97f8765d 100755 --- a/Gemfile +++ b/Gemfile @@ -40,7 +40,7 @@ gem 'geocoder', '~> 1.6.3' # for adding latitude and longitude to location-based gem 'i18n-js', '~> 3.3' gem 'lograge', '~> 0.11.2' # make logging less terrible in rails gem 'nearest_time_zone', '~> 0.0.4' # for detecting timezone from lat/lng https://github.com/buytruckload/nearest_time_zone -gem 'rails-i18n', '~> 6', '~> 6.0.0' +gem 'rails-i18n', '~> 6.0.0', '~> 6' gem 'roadie-rails', '~> 2.1' # email generation helpers gem 'table_print', '~> 1.5', '>= 1.5.6' # Nice table printing of data for the console From 5ceb05e6c3c8fbc5f64a125102383e01255b3654 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 4 Jun 2020 11:21:02 -0500 Subject: [PATCH 439/440] Set Geocoder to use the test provider on tests --- config/initializers/geocode.rb | 4 ++-- spec/rails_helper.rb | 12 ++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/config/initializers/geocode.rb b/config/initializers/geocode.rb index 0d7a0ed5..22d2dec8 100644 --- a/config/initializers/geocode.rb +++ b/config/initializers/geocode.rb @@ -3,8 +3,8 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later Geocoder.configure( cache: Rails.cache, - lookup: :google, + lookup: Rails.env == 'test' ? :test : :google, use_https: true, api_key: ENV['GOOGLE_API_KEY'], timeout: 10 -) +) \ No newline at end of file diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 8af99c06..478ebca1 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -72,4 +72,16 @@ RSpec.configure do |config| config.include Devise::Test::ControllerHelpers, type: :controller config.include Devise::Test::IntegrationHelpers, type: :request config.include RSpec::Rails::RequestExampleGroup, type: :request, file_path: %r{spec/api} + Geocoder::Lookup::Test.set_default_stub( + [ + { + 'coordinates' => [44.2876041,-88.4671082], + 'address' => 'Appleton, WI, USA', + 'state' => 'Appleton', + 'state_code' => 'WI', + 'country' => 'United States', + 'country_code' => 'US' + } + ] + ) end From 8e39ec6c49f086fad5b819a296701e24ee93d0b5 Mon Sep 17 00:00:00 2001 From: Eric Date: Thu, 4 Jun 2020 11:19:29 -0500 Subject: [PATCH 440/440] Add notices tasks and workflow --- .github/workflows/full_build.yml | 111 +++++++++++++++++++++++++++++++ bin/setup | 21 ++++-- lib/tasks/bundler_notice.rake | 22 ------ lib/tasks/notice.rake | 59 ++++++++++++++++ 4 files changed, 185 insertions(+), 28 deletions(-) create mode 100644 .github/workflows/full_build.yml delete mode 100644 lib/tasks/bundler_notice.rake create mode 100644 lib/tasks/notice.rake diff --git a/.github/workflows/full_build.yml b/.github/workflows/full_build.yml new file mode 100644 index 00000000..10bcec63 --- /dev/null +++ b/.github/workflows/full_build.yml @@ -0,0 +1,111 @@ +name: Houdini build +on: [push, pull_request] +jobs: + notice_ruby: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + - uses: actions/cache@v1 + name: Use Gem cache + with: + path: vendor/bundle + key: bundle-use-ruby-ubuntu-latest-2.6.6-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + bundle-use-ruby-ubuntu-latest-2.6.6- + - name: bundle install + run: | + bundle config deployment true + bundle config path vendor/bundle + bundle install --jobs 4 + - name: run notice:ruby:verify + run: | + bin/rails notice:ruby:verify + notice_js: + runs-on: ubuntu-latest + needs: notice_ruby + steps: + - uses: actions/checkout@v2 + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + - name: Setup Node.js environment + uses: actions/setup-node@v1.4.2 + - uses: actions/cache@v1 + name: Use Node package cache + with: + path: node_modules + key: bundle-use-node-js-ubuntu-latest-12-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + bundle-use-node-js-ubuntu-latest-12- + - uses: actions/cache@v1 + name: Use Gem cache + with: + path: vendor/bundle + key: bundle-use-ruby-ubuntu-latest-2.6.6-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + bundle-use-ruby-ubuntu-latest-2.6.6- + - name: bundle install + run: | + bundle config deployment true + bundle config path vendor/bundle + bundle install --jobs 4 + - name: install node packages + run: yarn install --frozen-lockfile + - name: run notice:js:verify + run: | + bin/rails notice:js:verify + main_build: + needs: [notice_js] + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest] + node: [12] + ruby: [2.6.6] + steps: + - uses: actions/checkout@v2 + - name: Setup PostgreSQL with PostgreSQL extensions and unprivileged user + uses: Daniel-Marynicz/postgresql-action@0.1.0 + with: + postgres_image_tag: 11-alpine + postgres_user: houdini_user + postgres_password: password + - name: Set up Ruby + # To automatically get bug fixes and new Ruby versions for ruby/setup-ruby, + # change this to (see https://github.com/ruby/setup-ruby#versioning): + # uses: ruby/setup-ruby@v1 + uses: ruby/setup-ruby@ec106b438a1ff6ff109590de34ddc62c540232e0 + - name: Setup Node.js environment + uses: actions/setup-node@v1.4.2 + - uses: actions/cache@v1 + name: Use Gem cache + with: + path: vendor/bundle + key: bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}-${{ hashFiles('**/Gemfile.lock') }} + restore-keys: | + bundle-use-ruby-${{ matrix.os }}-${{ matrix.ruby }}- + - name: bundle install + run: | + bundle config deployment true + bundle config path vendor/bundle + bundle install --jobs 4 + - uses: actions/cache@v1 + name: Use Node package cache + with: + path: node_modules + key: bundle-use-node-js-${{ matrix.os }}-${{ matrix.node }}-${{ hashFiles('**/yarn.lock') }} + restore-keys: | + bundle-use-node-js-${{ matrix.os }}-${{ matrix.node }}- + - name: run setup + run: bin/setup ci + - name: run spec + run: bin/rails spec + - name: run jest + run: yarn jest \ No newline at end of file diff --git a/bin/setup b/bin/setup index 39fcd7c5..233c15e7 100755 --- a/bin/setup +++ b/bin/setup @@ -19,15 +19,24 @@ FileUtils.chdir APP_ROOT do system('bundle check') || system!('bundle install') puts "\n== Installing dependencies (JS) ==" + yarn_cmd = 'yarn -s' + if ARGV.length > 0 && ARGV[0] == 'ci' + yarn_cmd += " --frozen-lockfile" + end system!('yarn -s') - puts "\n== Copying env file ==" - unless File.exist?('.env') - FileUtils.cp '.env.template', '.env' - end - puts "\n== Create new secrets ==" - File.write('.env', File.read('.env').gsub(/-- secret string --/) { SecureRandom.hex(64) }) + if ARGV.length == 0 || (ARGV.length > 0 && ARGV[0] != 'ci') + puts "\n== Copying env file ==" + unless File.exist?('.env') + FileUtils.cp '.env.template', '.env' + end + puts "\n== Create new secrets ==" + File.write('.env', File.read('.env').gsub(/-- secret string --/) { SecureRandom.hex(64) }) + else + puts "\n== Copying env file ==" + FileUtils.cp '.env.test', '.env' + end puts "\n== Preparing database ==" system! 'bin/rails db:prepare' diff --git a/lib/tasks/bundler_notice.rake b/lib/tasks/bundler_notice.rake deleted file mode 100644 index 23e3265b..00000000 --- a/lib/tasks/bundler_notice.rake +++ /dev/null @@ -1,22 +0,0 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -desc "generating a notice for bundler" - -# Clear old activerecord sessions tables daily -task :bundler_notice => :environment do - require 'bundler' - require 'httparty' - parser = Bundler::LockfileParser.new(File.read(Rails.root.join("Gemfile.lock"))) - result = parser.specs.map do |spec| - "gem/rubygems/-/#{spec.name}/#{spec.version.to_s}" - end - - @options = { - :headers => { - 'Content-Type' => 'application/json', - 'Accept' => 'application/json' - } - } - - result = HTTParty.post("https://api.clearlydefined.io/notices", @options.merge(body:JSON::generate({coordinates: result}))) - byebug -end \ No newline at end of file diff --git a/lib/tasks/notice.rake b/lib/tasks/notice.rake new file mode 100644 index 00000000..6bac145f --- /dev/null +++ b/lib/tasks/notice.rake @@ -0,0 +1,59 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +# Create notice files for dependencies +namespace :notice do + namespace :ruby do + require 'bundler' + require 'httparty' + def get_notice_ruby + parser = Bundler::LockfileParser.new(File.read(Rails.root.join("Gemfile.lock"))) + result = parser.specs.map do |spec| + "gem/rubygems/-/#{spec.name}/#{spec.version.to_s}" + end + + @options = { + :headers => { + 'Content-Type' => 'application/json', + 'Accept' => 'application/json' + }, + :timeout => 120 + } + result = HTTParty.post("https://api.clearlydefined.io/notices", @options.merge(body:JSON::generate({coordinates: result}))) + end + + desc "generating NOTICE-ruby from ClearlyDefined.io" + task :update do + result = get_notice_ruby + File.write('NOTICE-ruby', result.body) + end + + desc "checking whether NOTICE-ruby matches the one on ClearlyDefined.io" + task :verify do + result = get_notice_ruby + raise "NOTICE-ruby is not up to date. Run bin/rails notice:ruby:update to update the file." if result.body != File.read('NOTICE-ruby') + end + end + + namespace :js do + require 'fileutils' + def get_notice_js + raise "NOTICE-js could not be retrieved from Clearlydefined.io" unless system('yarn noticeme') + File.read('NOTICE') + end + + desc "generating NOTICE-js from ClearlyDefined.io" + task :update do + if (File.exists?('NOTICE')) + File.delete('NOTICE') + end + result = get_notice_js + FileUtils.mv('NOTICE', 'NOTICE-js', force: true) + end + + desc "checking whether NOTICE-js matches the one on ClearlyDefined.io" + task :verify do + result = get_notice_js + raise "NOTICE-js is not up to date. Run bin/rails notice:js:update to update the file." if result != File.read('NOTICE-js') + end + end +end \ No newline at end of file

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 I#ppBt8$tz9^#A|> diff --git a/app/assets/fonts/Open_Sans/opensans-light-webfont.eot b/app/assets/fonts/Open_Sans/opensans-light-webfont.eot deleted file mode 100644 index 77dc71d6dbf4247f204e4a1f0c7968377b4e9ce2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20550 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 emLyN8sUq(`3<{dFN#9_z!n{H6kBRHGk|N;1 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Open_Sans/opensans-light-webfont.ttf b/app/assets/fonts/Open_Sans/opensans-light-webfont.ttf deleted file mode 100644 index 209e05248d8cac40395f181c6172689ad26995f6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 44484 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 Py2GvL^mUgu>?HaxMZjNS diff --git a/app/assets/fonts/Open_Sans/opensans-regular-webfont.eot b/app/assets/fonts/Open_Sans/opensans-regular-webfont.eot deleted file mode 100644 index 17e3094507d9c66d4fec034b2e870b37ba5d772c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21102 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 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Open_Sans/opensans-regular-webfont.ttf b/app/assets/fonts/Open_Sans/opensans-regular-webfont.ttf deleted file mode 100644 index c6413c2a744daf920470ced3fcb45f165211cbc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 45112 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 Gi2euEl`!c5 diff --git a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.eot b/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.eot deleted file mode 100644 index 781275130c7f9400c8e9384421c51721600eb276..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21980 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 D?ZOUj diff --git a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.svg b/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.svg deleted file mode 100644 index a65a2fd3..00000000 --- a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.svg +++ /dev/null @@ -1,1398 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.ttf b/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.ttf deleted file mode 100644 index 384a91c9917146f70bec6260a047c313b8803ed9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 47336 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