diff --git a/.babelrc b/.babelrc index bb75829cb..62c3ee729 100644 --- a/.babelrc +++ b/.babelrc @@ -1,5 +1,6 @@ { "compact":false, "presets": -["babel-preset-env"] +["babel-preset-env"], +"plugins": ["babel-plugin-transform-object-rest-spread"] } diff --git a/.bootstraprc b/.bootstraprc index 1327a743f..12541e08d 100644 --- a/.bootstraprc +++ b/.bootstraprc @@ -4,6 +4,7 @@ "extractStyles": true, "styles": { "mixins": true, + "glyphicons": true, "grid": true, "forms": true, "input-groups": true, diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 000000000..83f304afb --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,5 @@ +ie 11 +> 0.25% +last 2 chrome version +last 2 firefox version +last 2 safari version diff --git a/.dockerignore b/.dockerignore deleted file mode 100644 index 6e57e57fa..000000000 --- a/.dockerignore +++ /dev/null @@ -1,9 +0,0 @@ -* -!Gemfile -!Gemfile.lock -!package.json -!package-lock.json -!script/build/debian/*.sh -!Rakefile -!config/* -!gems/* \ No newline at end of file diff --git a/.env.template b/.env.template index b6128da65..73dafc3b7 100644 --- a/.env.template +++ b/.env.template @@ -1,5 +1,7 @@ export DEVISE_SECRET_KEY='-- secret string --' #bundle exec rake secret export SECRET_TOKEN='-- secret string --' #bundle exec rake secret +export SECRET_KEY_BASE='-- secret string --' + export STRIPE_API_KEY='REPLACE' # use your test private key from your stripe account export STRIPE_API_PUBLIC='REPLACE' # use your test public key from your stripe account export S3_BUCKET_NAME='REPLACE' diff --git a/.env.test b/.env.test index 99aee4972..ddd2ccd49 100644 --- a/.env.test +++ b/.env.test @@ -1,5 +1,6 @@ export DEVISE_SECRET_KEY='0696452e54b14758b8534437d8cf418ea920ff23bb9c3a061a9ab2827bab4685710e89b899ebd4457df600cb5f2e04adb6a0fdef09a6a45558ecae1d6906f4a6' #bundle exec rake secret export SECRET_TOKEN='0696452e54b14758b8534437d8cf418ea920ff23bb9c3a061a9ab2827bab4685710e89b899ebd4457df600cb5f2e04adb6a0fdef09a6a45558ecae1d6906f4a6' #bundle exec rake secret +export SECRET_KEY_BASE='0696452e54b14758b8534437d8cf418ea920ff23bb9c3a061a9ab2827bab4685710e89b899ebd4457df600cb5f2e04adb6a0fdef09a6a45558ecae1d6906f4a6' #bundle exec rake secret export STRIPE_API_KEY='REPLACE' # use your test private key from your stripe account export STRIPE_API_PUBLIC='REPLACE' # use your test public key from your stripe account diff --git a/.foreman b/.foreman new file mode 100644 index 000000000..443487920 --- /dev/null +++ b/.foreman @@ -0,0 +1,2 @@ +port: 5000 +procfile: Procfile.dev diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000..5dbfd7fce --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,3 @@ +# make tabs uniform +2a321748229282e853bd979e7b73f16c04a0283a + diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..0c81edbeb --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,24 @@ +# To get started with Dependabot version updates, you'll need to specify which +# package ecosystems to update and where the package manifests are located. +# Please see the documentation for all configuration options: +# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates + +version: 2 +updates: + - package-ecosystem: "github-actions" # See documentation for possible values + directory: "/" # Location of package manifests + schedule: + interval: "weekly" + + - package-ecosystem: "bundler" + directory: "/" + allow: + - dependency-type: "development" + schedule: + interval: "weekly" + groups: + development-dependencies: + dependency-type: "development" + update-types: + - "minor" + - "patch" diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..eb3571edd --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,2 @@ +**NOTE: DO NOT discuss internal CommitChange information in your PR; this PR will be public. +Link back to the issue in the Tix repo when you need to do that.** diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 000000000..a3f567809 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,80 @@ + +name: Main build +on: + pull_request: + types: [opened, reopened, synchronize] + push: + branches: ["supporter_level_goal"] +concurrency: + group: build--${{ github.head_ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04] + node: [14.19.1] + ruby: ['2.6.10'] + fail-fast: false + steps: + - uses: actions/checkout@v4 + - uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + ruby: + - 'app/**' + - 'bin/**' + - 'config/**' + - 'db/**' + - 'gems/**' + - 'lib/**' + - 'public/**' + - 'script/**' + - 'spec/**' + - '.ruby-version' + - '.rspec' + - 'config.ru' + - 'Gemfile' + - 'Gemfile.lock' + - 'Rakefile' + js: + - '**/*.js*' + - '**/*.es6' + - '**/*.ts*' + - '**/*.json' + - package.json + - yarn.lock + - '.nvmrc' + - '.babelrc' + - '.bootstraprc' + - '.browserlistrc' + + - name: Setup PostgreSQL with PostgreSQL extensions and unprivileged user + uses: Daniel-Marynicz/postgresql-action@1.0.0 + with: + postgres_image_tag: 12-alpine + postgres_user: admin + postgres_password: password + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + - name: set CUSTOM_RUBY_VERSION environment variable + run: echo "CUSTOM_RUBY_VERSION=${{ matrix.ruby }}" >> $GITHUB_ENV + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - run: bin/setup + - if: steps.changes.outputs.ruby == 'true' + name: run spec + run: bin/rake spec + - if: steps.changes.outputs.ruby == 'true' + run: script/compile-assets.sh + - if: steps.changes.outputs.js == 'true' + run: yarn build + - if: steps.changes.outputs.js == 'true' + run: yarn jest diff --git a/.github/workflows/dependent-issues.yml b/.github/workflows/dependent-issues.yml new file mode 100644 index 000000000..d273e897b --- /dev/null +++ b/.github/workflows/dependent-issues.yml @@ -0,0 +1,53 @@ +# License: LGPL-3.0-or-later +name: Dependent Issues + +on: + issues: + types: + - opened + - edited + - reopened + pull_request_target: + types: + - opened + - edited + - reopened + # Makes sure we always add status check for PRs. Useful only if + # this action is required to pass before merging. Can be removed + # otherwise. + - synchronize + + +jobs: + check: + runs-on: ubuntu-latest + permissions: + actions: none + checks: none + contents: none + deployments: none + issues: write + discussions: none + packages: none + pull-requests: write + repository-projects: none + security-events: none + statuses: write + steps: + - uses: z0al/dependent-issues@v1.5.2 + env: + # (Required) The token to use to make API calls to GitHub. + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + # (Optional) The token to use to make API calls to GitHub for remote repos. + GITHUB_READ_TOKEN: ${{ secrets.GITHUB_READ_TOKEN }} + with: + # (Optional) The label to use to mark dependent issues + label: dependent + + # (Optional) Enable checking for dependencies in issues. + # Enable by setting the value to "on". Default "off" + check_issues: off + + # (Optional) A comma-separated list of keywords. Default + # "depends on, blocked by" + keywords: depends on, blocked by \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 000000000..3fc1e0a78 --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,74 @@ +name: Deploy +on: + workflow_dispatch: + # Inputs the workflow accepts. + inputs: + mode: + description: Mode to build + required: true + default: 'staging' + type: choice + options: + - staging + version_tag: + description: New Version To Create + required: true + commit_being_built: + description: SHA of the original git version being built + required: true +concurrency: + group: deploy--${{ github.head_ref }} + cancel-in-progress: true +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-20.04] + node: [14.19.1] + ruby: ['2.6.10'] + fail-fast: false + steps: + - name: 'Checkout our repo' + uses: actions/checkout@v4 + with: + token: ${{ secrets.PAT_TO_RUN_AS_ERIC }} + + - name: Setup PostgreSQL with PostgreSQL extensions and unprivileged user + uses: Daniel-Marynicz/postgresql-action@1.0.0 + with: + postgres_image_tag: 12-alpine + postgres_user: admin + postgres_password: password + - uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node }} + cache: 'yarn' + - name: set environment variables + run: echo "CUSTOM_RUBY_VERSION=${{ matrix.ruby }}" >> $GITHUB_ENV + - uses: ruby/setup-ruby@v1 + with: + ruby-version: ${{ matrix.ruby }} + bundler-cache: true + + - run: bin/setup + - env: + mode: ${{ github.event.inputs.mode }} + ORG_NAME: commitchange + run: yarn build-all-${mode} + - name: "Push deploy" + env: + mode: ${{ github.event.inputs.mode }} + NewVersionTag: ${{ github.event.inputs.version_tag }} + commit_being_built: ${{github.event.inputs.commit_being_built}} + run: | + + git add public -f + git config --global user.email "robot@commitchange.com" + git config --global user.name "Robot" + git commit -m "Deployed version of $commit_being_built" + + git tag $NewVersionTag-$mode-release-deploy + git push origin $NewVersionTag-$mode-release-deploy + + git push origin HEAD:staging_deploy -f \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2a4f41ce8..355eb68c0 100755 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,15 @@ javascripts/api !public/css/donate-button.v2.css !public/svgs !public/svgs/* + +/database +.byebug_history + +/payouts + +# yard related directories +/ruby_docs +/.yardoc + +# Data files you wouldn't want to push +*.csv \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index a83d20d30..000000000 --- a/.jshintrc +++ /dev/null @@ -1,5 +0,0 @@ -{ -"esversion":6, -"asi" : true, -"laxcomma": true -} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 000000000..49319ad46 --- /dev/null +++ b/.nvmrc @@ -0,0 +1,2 @@ +14.19.1 + diff --git a/.ruby-version b/.ruby-version index 00355e29d..a04abec91 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.3.7 +2.6.10 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 0545e24a4..000000000 --- a/.travis.yml +++ /dev/null @@ -1,13 +0,0 @@ -sudo: required - -language: minimal - -services: -- docker - -before_install: -- docker-compose -f docker/build/docker-compose.yml build -- cp .env.test .env - -script: -- docker-compose -f docker/build/docker-compose.yml run -e RACK_ENV=ci -e RAILS_ENV=ci -e BUILD_DATABASE_URL=postgres://admin:password@db/commitchange_development build script/test.sh diff --git a/.yardopts b/.yardopts new file mode 100644 index 000000000..7081e3806 --- /dev/null +++ b/.yardopts @@ -0,0 +1 @@ +-m markdown -o ./ruby_docs \ No newline at end of file diff --git a/CCS_HASH b/CCS_HASH new file mode 100644 index 000000000..cebfb6ae2 --- /dev/null +++ b/CCS_HASH @@ -0,0 +1 @@ +ee1902e3b97d7dbec341371b24749ba0e17c069a diff --git a/Gemfile b/Gemfile old mode 100755 new mode 100644 index abe7d29aa..6f73f9fc2 --- a/Gemfile +++ b/Gemfile @@ -1,50 +1,50 @@ source 'https://rubygems.org' -ruby '2.3.7' +ruby ENV['CUSTOM_RUBY_VERSION'] || '2.6.10' # heroku needs a specific ruby version in the Gemfile + gem 'rake' -gem 'rails', '3.2.22.5' -gem 'rails_12factor' -# https://stripe.com/docs/api -gem 'stripe' +gem 'rails', '~> 4.0' + +gem 'rack', git: "https://github.com/CommitChange/rack.git", branch: "1-6-stable" -# Compression of assets on heroku -# https://github.com/romanbsd/heroku-deflater -gem 'heroku-deflater', :group => :production +gem 'date', '~> 2.0.3' + +# https://stripe.com/docs/api +gem 'stripe', '~> 4' # json serialization # https://github.com/nesquena/rabl gem 'rabl' -gem 'parallel' +gem 'jbuilder' + +gem "puma", "~> 5.6" + +gem 'kaminari' -gem 'puma' gem 'bootsnap', require: false gem 'rack-timeout' gem 'puma_worker_killer' -gem 'test-unit', '~> 3.0' +gem 'test-unit' gem 'hamster' -gem 'aws-ses' -gem 'aws-sdk' +gem 'aws-sdk-s3' +gem 'aws-sdk-rails' + +gem 'json', '>= 2.3.0' + # 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 -# https://github.com/ericallam/font_assets -gem 'font_assets', '~> 0.1.14' +# to find middleware thread safety bugs +gem 'rack-freeze' # Database (postgres) -gem 'pg' # Postgresql +gem 'pg', "< 1" # Postgresql, must be under 1 because 1.0 and later don't work on Rails 4 gem 'qx', path: 'gems/ruby-qx' gem 'dalli' -gem 'memcachier' gem 'param_validation', path: 'gems/ruby-param-validation' @@ -55,16 +55,12 @@ gem 'colorize' # https://github.com/collectiveidea/delayed_job_active_record gem 'delayed_job_active_record' -# for styling emails -# https://github.com/Mange/roadie-rails -gem 'roadie-rails' - # For nat lang parsing of dates gem 'chronic' # Images # https://github.com/carrierwaveuploader/carrierwave -gem 'carrierwave' +gem 'carrierwave', '~> 1', '< 2' gem 'carrierwave-aws' # for uploading images to amazon s3 gem 'mini_magick' @@ -73,8 +69,10 @@ gem 'httparty' # User authentication # https://github.com/plataformatec/devise -gem 'devise' -gem 'devise-async' +gem 'devise', '~> 4.1' + +# https://github.com/airbrake/airbrake +gem 'airbrake' # http://www.rubygeocoder.com/ gem 'geocoder' # for adding latitude and longitude to location-based tables @@ -82,51 +80,56 @@ gem 'geocoder' # for adding latitude and longitude to location-based tables # https://github.com/buytruckload/nearest_time_zone gem 'nearest_time_zone' # for detecting timezone from lat/lng -gem 'mail_view' +gem 'rest-client' # recommended for fullcontact -gem 'fullcontact' # Full Contact API; includes #Hashie::Mash +# https://github.com/fphilipe/premailer-rails +# for stylizing emails +gem 'premailer-rails' # Nice table printing of data for the console gem 'table_print' -gem 'bunny', '>= 2.6.3' - -gem 'rails-i18n', '~> 3.0.0' # For 3.x +gem 'rails-i18n' # For 4.0.x gem 'i18n-js' gem 'countries' group :development, :ci do gem 'traceroute' - gem 'debase' - gem 'ruby-debug-ide' end group :development, :ci, :test do gem 'timecop' gem 'pry' - #gem 'pry-byebug' + gem 'pry-byebug' gem 'binding_of_caller' - gem 'rspec' - gem 'rspec-rails' + gem 'rspec', "~> 3" + gem 'rspec-rails', "~> 4" 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 'stripe-ruby-mock', '~> 2.5.1', :require => 'stripe_mock' gem 'factory_bot' gem 'factory_bot_rails' - gem 'action_mailer_matchers' + gem 'action_mailer_matchers', '~> 1.2.0' gem 'simplecov', '~> 0.16.1', require: false + gem 'byebug' + gem 'shoulda-matchers' + gem 'rspec-json_expectations' + gem 'yard' + gem 'faker' # test data generation end + +gem 'nokogiri', '~> 1.13.11', require: false, git:"https://github.com/commitchange/nokogiri.git", tag: "v1.13.11" + + group :test do gem 'webmock' end # Gems used for asset compilation -gem 'sass', '3.2.19' -gem 'sass-rails', '3.2.6' -gem 'uglifier' +gem 'sassc' +gem 'sassc-rails' # make logging less terrible in rails gem 'lograge' @@ -136,10 +139,49 @@ gem 'dry-validation' # used only for config validation gem 'foreman' + + +group :production do + gem 'rails_autoscale_agent', '>= 0.9.1' + gem 'tunemygc' +end + + +group :production, :staging do + gem 'heroku_rails_deflate' + gem "hiredis", "~> 0.6.0" + gem "redis", ">= 3.2.0" + gem 'redis-actionpack' + gem 'rails_12factor' +end + gem 'grape', '~> 1.1.0' gem 'grape-entity', git: 'https://github.com/ruby-grape/grape-entity.git', ref: '0e04aa561373b510c2486282979085eaef2ae663' gem 'grape-swagger' gem 'grape-swagger-entity' gem 'grape_url_validator' gem 'grape_logging' -gem 'grape_devise', path: 'gems/grape_devise' + +gem 'recaptcha', '~> 5.8.1' + +gem 'hashie' + +gem 'connection_pool' + +gem "barnes" + +gem 'protected_attributes' # because we upgraded from 3 + +gem 'actionpack-action_caching' # because we use action caching + +gem 'rack-cors' + +gem 'ruby2_keywords' # needed because we're backporting code from Rails 6.2 + +gem 'securerandom' # needed becuase we're on a pre-2.5 Ruby version + +gem 'fx', git: 'https://github.com/teoljungberg/fx.git', ref: '946cdccbd12333deb8f4566c9852b49c0231a618' + +gem 'has_scope' + +gem 'globalid', git: "https://github.com/CommitChange/globalid.git", tag: "0.4.2.1" \ No newline at end of file diff --git a/Gemfile.lock b/Gemfile.lock index 113acbf75..932cab738 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -1,12 +1,26 @@ GIT - remote: https://github.com/commitchange/stripe-ruby-mock.git - revision: ee4471a8f654672d5596218c2b68a2913ea3f4cc - branch: 2.4.1 + remote: https://github.com/CommitChange/globalid.git + revision: add4f61a0ccf49704ab03dc7505a8727655720a7 + tag: 0.4.2.1 specs: - stripe-ruby-mock (2.4.1) - dante (>= 0.2.0) - multi_json (~> 1.0) - stripe (>= 1.31.0, <= 1.58.0) + globalid (0.4.2.1) + activesupport (>= 4.2.0) + +GIT + remote: https://github.com/CommitChange/rack.git + revision: d48d04ed52ed3f196906c2e1c988879b8361c939 + branch: 1-6-stable + specs: + rack (1.6.13) + +GIT + remote: https://github.com/commitchange/nokogiri.git + revision: 845f33399b3063f8f1f6e73a3035ff265ae3965b + tag: v1.13.11 + specs: + nokogiri (1.13.11) + mini_portile2 (~> 2.8.0) + racc (~> 1.4) GIT remote: https://github.com/ruby-grape/grape-entity.git @@ -17,13 +31,14 @@ GIT activesupport (>= 3.0.0) multi_json (>= 1.3.2) -PATH - remote: gems/grape_devise +GIT + remote: https://github.com/teoljungberg/fx.git + revision: 946cdccbd12333deb8f4566c9852b49c0231a618 + ref: 946cdccbd12333deb8f4566c9852b49c0231a618 specs: - grape_devise (0.1.1) - devise (>= 2.2.8, < 5) - grape (> 0.7) - rails (> 3.2, < 6) + fx (0.6.2) + activerecord (>= 4.0.0) + railties (>= 4.0.0) PATH remote: gems/ruby-param-validation @@ -41,80 +56,106 @@ PATH 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) + action_mailer_matchers (1.2.0) + actionmailer (4.2.11.3) + actionpack (= 4.2.11.3) + actionview (= 4.2.11.3) + activejob (= 4.2.11.3) + mail (~> 2.5, >= 2.5.4) + rails-dom-testing (~> 1.0, >= 1.0.5) + actionpack (4.2.11.3) + actionview (= 4.2.11.3) + activesupport (= 4.2.11.3) + rack (~> 1.6) + rack-test (~> 0.6.2) + rails-dom-testing (~> 1.0, >= 1.0.5) + rails-html-sanitizer (~> 1.0, >= 1.0.2) + actionpack-action_caching (1.2.1) + actionpack (>= 4.0.0) + actionview (4.2.11.3) + activesupport (= 4.2.11.3) + builder (~> 3.1) 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) - 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.3) + activejob (4.2.11.3) + activesupport (= 4.2.11.3) + globalid (>= 0.3.0) + activemodel (4.2.11.3) + activesupport (= 4.2.11.3) + builder (~> 3.1) + activerecord (4.2.11.3) + activemodel (= 4.2.11.3) + activesupport (= 4.2.11.3) + arel (~> 6.0) + activesupport (4.2.11.3) + i18n (~> 0.7) + minitest (~> 5.1) + thread_safe (~> 0.3, >= 0.3.4) + tzinfo (~> 1.1) + addressable (2.8.0) + public_suffix (>= 2.0.2, < 5.0) + airbrake (10.0.2) + airbrake-ruby (~> 4.13) + airbrake-ruby (4.13.3) + rbtree3 (~> 0.5) 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) - json (~> 1.4) - nokogiri (>= 1.4.4) - aws-ses (0.6.0) - builder - mail (> 2.2.5) - mime-types - xml-simple + arel (6.0.4) + aws-eventstream (1.2.0) + aws-partitions (1.579.0) + aws-sdk-core (3.130.1) + aws-eventstream (~> 1, >= 1.0.2) + aws-partitions (~> 1, >= 1.525.0) + aws-sigv4 (~> 1.1) + jmespath (~> 1.0) + aws-sdk-kms (1.56.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sdk-rails (2.1.0) + aws-sdk-ses (~> 1) + railties (>= 3) + aws-sdk-s3 (1.113.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sdk-kms (~> 1) + aws-sigv4 (~> 1.4) + aws-sdk-ses (1.47.0) + aws-sdk-core (~> 3, >= 3.127.0) + aws-sigv4 (~> 1.1) + aws-sigv4 (1.5.0) + aws-eventstream (~> 1, >= 1.0.2) 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) + barnes (0.0.8) + multi_json (~> 1) + statsd-ruby (~> 1.1) + bcrypt (3.1.16) binding_of_caller (0.7.2) debug_inspector (>= 0.0.1) bootsnap (1.1.7) 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) + builder (3.2.4) + byebug (11.1.3) + carrierwave (1.3.2) + activemodel (>= 4.0.0) + activesupport (>= 4.0.0) mime-types (>= 1.16) - carrierwave-aws (0.5.0) - aws-sdk (~> 1.58) - carrierwave (~> 0.7) + ssrf_filter (~> 1.0) + carrierwave-aws (1.4.0) + aws-sdk-s3 (~> 1.0) + carrierwave (>= 0.7, < 2.1) chronic (0.10.2) - coderay (1.1.2) + coderay (1.1.3) coercible (1.0.0) descendants_tracker (~> 0.0.1) colorize (0.8.1) - concurrent-ruby (1.0.5) + concurrent-ruby (1.2.2) config (1.7.0) activesupport (>= 3.0) deep_merge (~> 1.2.1) dry-validation (>= 0.10.4) + connection_pool (2.2.2) countries (2.1.2) i18n_data (~> 0.8.0) money (~> 6.9) @@ -122,35 +163,31 @@ GEM unicode_utils (~> 1.4) crack (0.4.2) safe_yaml (~> 1.0.0) - css_parser (1.3.6) + crass (1.0.6) + css_parser (1.9.0) addressable - dalli (2.7.6) + dalli (3.2.3) dante (0.2.0) database_cleaner (1.6.1) - debase (0.2.2) - debase-ruby_core_source (>= 0.10.2) - debase-ruby_core_source (0.10.3) + date (2.0.3) debug_inspector (0.0.2) deep_merge (1.2.1) - delayed_job (4.1.2) - activesupport (>= 3.0, < 5.1) + delayed_job (4.1.10) + activesupport (>= 3.0, < 8.0) delayed_job_active_record (4.1.1) activerecord (>= 3.0, < 5.1) delayed_job (>= 3.0, < 5) descendants_tracker (0.0.4) thread_safe (~> 0.3, >= 0.3.1) - devise (3.5.10) + devise (4.7.3) bcrypt (~> 3.0) orm_adapter (~> 0.1) - railties (>= 3.2.6, < 5) + railties (>= 4.1.0) responders - thread_safe (~> 0.1) warden (~> 1.2.3) - devise-async (0.9.0) - devise (~> 3.2) - diff-lcs (1.2.5) + diff-lcs (1.5.0) docile (1.3.1) - domain_name (0.5.20160615) + domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) dotenv (2.0.1) dotenv-rails (2.0.1) @@ -184,27 +221,20 @@ GEM dry-types (~> 0.12.0) equalizer (0.0.11) erubis (2.7.0) - execjs (2.5.2) - factory_bot (4.8.2) - activesupport (>= 3.0.0) - factory_bot_rails (4.8.2) - factory_bot (~> 4.8.2) - railties (>= 3.0.0) - faraday (0.9.1) + factory_bot (5.2.0) + activesupport (>= 4.2.0) + factory_bot_rails (5.2.0) + factory_bot (~> 5.2.0) + railties (>= 4.2.0) + faker (2.2.1) + i18n (>= 0.8) + faraday (0.17.3) 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) - thor (~> 0.19.1) - fullcontact (0.9.0) - faraday (~> 0.9.0) - faraday_middleware (>= 0.9) - hashie (>= 2.0, < 4.0) - plissken + ffi (1.11.3) + foreman (0.87.2) geocoder (1.2.11) - get_process_mem (0.2.1) + get_process_mem (0.2.7) + ffi (~> 1.0) grape (1.1.0) activesupport builder @@ -224,14 +254,21 @@ GEM grape (>= 0.12.0) hamster (3.0.0) concurrent-ruby (~> 1.0) - hashie (3.4.1) - heroku-deflater (0.5.3) + has_scope (0.7.2) + actionpack (>= 4.1) + activesupport (>= 4.1) + hashie (4.0.0) + heroku_rails_deflate (1.0.3) + actionpack (>= 3.2.13) + activesupport (>= 3.2.13) rack (>= 1.4.5) - hike (1.2.3) - http-cookie (1.0.2) + hiredis (0.6.3) + htmlentities (4.3.4) + http-accept (1.7.0) + http-cookie (1.0.3) domain_name (~> 0.5) - httparty (0.13.3) - json (~> 1.8) + httparty (0.21.0) + mini_mime (>= 1.0.0) multi_xml (>= 0.5.2) i18n (0.9.5) concurrent-ruby (~> 1.0) @@ -240,29 +277,46 @@ GEM i18n_data (0.8.0) ice_nine (0.11.2) inflecto (0.0.2) - journey (1.0.4) - json (1.8.6) + jbuilder (2.9.1) + activesupport (>= 4.2.0) + jmespath (1.6.1) + json (2.6.1) + kaminari (1.2.1) + activesupport (>= 4.1.0) + kaminari-actionview (= 1.2.1) + kaminari-activerecord (= 1.2.1) + kaminari-core (= 1.2.1) + kaminari-actionview (1.2.1) + actionview + kaminari-core (= 1.2.1) + kaminari-activerecord (1.2.1) + activerecord + kaminari-core (= 1.2.1) + kaminari-core (1.2.1) 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) - mail_view (2.0.4) - tilt - memcachier (0.0.2) - method_source (0.9.0) - mime-types (1.25.1) + loofah (2.21.3) + crass (~> 1.0.2) + nokogiri (>= 1.12.0) + mail (2.7.1) + mini_mime (>= 0.1.1) + method_source (1.0.0) + mime-types (3.4.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2022.0105) mini_magick (4.9.5) - mini_portile2 (2.1.0) + mini_mime (1.1.0) + mini_portile2 (2.8.2) + minitest (5.18.0) money (6.10.0) i18n (>= 0.6.4, < 1.0) msgpack (1.2.0) - multi_json (1.13.1) - multi_xml (0.5.5) - multipart-post (2.0.0) + multi_json (1.15.0) + multi_xml (0.6.0) + multipart-post (2.1.1) mustermann (1.0.3) mustermann-grape (1.0.0) mustermann (~> 1.0.0) @@ -270,142 +324,175 @@ GEM andand kdtree require_all + net-http-persistent (3.1.0) + connection_pool (~> 2.2) netrc (0.11.0) - nokogiri (1.6.8.1) - mini_portile2 (~> 2.1.0) + nio4r (2.5.9) orm_adapter (0.5.0) - parallel (1.6.1) - pg (0.18.3) - plissken (0.2.0) - symbolize (~> 4.2) - polyglot (0.3.5) + pg (0.21.0) power_assert (1.1.1) - pry (0.11.3) - coderay (~> 1.1.0) - method_source (~> 0.9.0) - puma (3.11.2) - puma_worker_killer (0.1.0) + premailer (1.12.1) + addressable + css_parser (>= 1.6.0) + htmlentities (>= 4.0.0) + premailer-rails (1.11.1) + actionmailer (>= 3) + premailer (~> 1.7, >= 1.7.9) + protected_attributes (1.1.4) + activemodel (>= 4.0.1, < 5.0) + pry (0.13.1) + coderay (~> 1.1) + method_source (~> 1.0) + pry-byebug (3.9.0) + byebug (~> 11.0) + pry (~> 0.13.0) + public_suffix (4.0.6) + puma (5.6.7) + nio4r (~> 2.0) + puma_worker_killer (0.3.1) get_process_mem (~> 0.2) - puma (>= 2.7, < 4) + puma (>= 2.7) rabl (0.11.6) activesupport (>= 2.3.14) - rack (1.4.7) + racc (1.6.2) 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 + rack-attack (6.5.0) + rack (>= 1.0, < 3) + rack-cors (1.0.6) + rack (>= 1.6.0) + rack-freeze (1.0.0) 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) + rails (4.2.11.3) + actionmailer (= 4.2.11.3) + actionpack (= 4.2.11.3) + actionview (= 4.2.11.3) + activejob (= 4.2.11.3) + activemodel (= 4.2.11.3) + activerecord (= 4.2.11.3) + activesupport (= 4.2.11.3) + bundler (>= 1.3.0, < 2.0) + railties (= 4.2.11.3) + sprockets-rails + rails-deprecated_sanitizer (1.0.4) + 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.5.0) + loofah (~> 2.19, >= 2.19.1) + rails-i18n (4.0.9) + i18n (~> 0.7) + railties (~> 4.0) 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_autoscale_agent (0.9.1) + rails_serve_static_assets (0.0.5) + rails_stdout_logging (0.0.5) + railties (4.2.11.3) + actionpack (= 4.2.11.3) + activesupport (= 4.2.11.3) rake (>= 0.8.7) - rdoc (~> 3.4) - thor (>= 0.14.6, < 2.0) - rake (12.3.1) - rdoc (3.12.2) - json (~> 1.4) + thor (>= 0.18.1, < 2.0) + rake (13.0.6) + rbtree3 (0.6.0) + recaptcha (5.8.1) + json + redis (4.2.5) + redis-actionpack (5.1.0) + actionpack (>= 4.0, < 7) + redis-rack (>= 1, < 3) + redis-store (>= 1.1.0, < 2) + redis-rack (2.0.6) + rack (>= 1.5, < 3) + redis-store (>= 1.2, < 2) + redis-store (1.9.0) + redis (>= 4, < 5) require_all (1.3.2) - responders (1.1.2) - railties (>= 3.2, < 4.2) - rest-client (1.8.0) + responders (2.4.1) + actionpack (>= 4.2.0, < 6.0) + railties (>= 4.2.0, < 6.0) + rest-client (2.1.0) + http-accept (>= 1.7.0, < 2.0) 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) + rspec (3.12.0) + rspec-core (~> 3.12.0) + rspec-expectations (~> 3.12.0) + rspec-mocks (~> 3.12.0) + rspec-core (3.12.2) + rspec-support (~> 3.12.0) + rspec-expectations (3.12.3) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-mocks (3.5.0) + rspec-support (~> 3.12.0) + rspec-json_expectations (2.2.0) + rspec-mocks (3.12.5) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.5.0) - rspec-rails (3.5.0) - 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) - ruby-debug-ide (0.6.1) - rake (>= 0.8.1) - ruby-prof (0.15.9) + rspec-support (~> 3.12.0) + rspec-rails (4.1.2) + actionpack (>= 4.2) + activesupport (>= 4.2) + railties (>= 4.2) + rspec-core (~> 3.10) + rspec-expectations (~> 3.10) + rspec-mocks (~> 3.10) + rspec-support (~> 3.10) + rspec-support (3.12.0) + ruby2_keywords (0.0.5) 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) + sassc (2.4.0) + ffi (~> 1.9) + sassc-rails (2.1.2) + railties (>= 4.0.0) + sassc (>= 2.0) + sprockets (> 3.0) + sprockets-rails + tilt + securerandom (0.1.1) + shoulda-matchers (4.5.1) + activesupport (>= 4.2.0) 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) + sprockets (3.7.2) + concurrent-ruby (~> 1.0) + rack (> 1, < 3) + sprockets-rails (3.2.2) + actionpack (>= 4.0) + activesupport (>= 4.0) + sprockets (>= 3.0.0) + ssrf_filter (1.0.7) + statsd-ruby (1.4.0) + stripe (4.24.0) + faraday (~> 0.13) + net-http-persistent (~> 3.0) + stripe-ruby-mock (2.5.8) + dante (>= 0.2.0) 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 + stripe (>= 2.0.3) table_print (1.5.4) test-unit (3.2.7) power_assert - thor (0.19.4) + thor (1.2.2) thread_safe (0.3.6) - tilt (1.4.1) + tilt (2.0.10) timecop (0.7.3) traceroute (0.5.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) + tunemygc (1.0.71) + tzinfo (1.2.11) + thread_safe (~> 0.1) unf (0.1.4) unf_ext - unf_ext (0.0.7.2) + unf_ext (0.0.7.7) unicode_utils (1.4.0) virtus (1.0.5) axiom-types (~> 0.1) @@ -417,89 +504,109 @@ GEM webmock (1.21.0) addressable (>= 2.3.6) crack (>= 0.3.2) - xml-simple (1.1.5) + webrick (1.7.0) + yard (0.9.27) + webrick (~> 1.7.0) PLATFORMS ruby DEPENDENCIES - action_mailer_matchers - aws-sdk - aws-ses + action_mailer_matchers (~> 1.2.0) + actionpack-action_caching + airbrake + aws-sdk-rails + aws-sdk-s3 + barnes binding_of_caller bootsnap - browserify-rails - bunny (>= 2.6.3) - carrierwave + byebug + carrierwave (~> 1, < 2) carrierwave-aws chronic colorize config (> 1.5) + connection_pool countries dalli database_cleaner - debase + date (~> 2.0.3) delayed_job_active_record - devise - devise-async + devise (~> 4.1) dotenv-rails dry-validation factory_bot factory_bot_rails - font_assets (~> 0.1.14) + faker foreman - fullcontact + fx! geocoder + globalid! grape (~> 1.1.0) grape-entity! grape-swagger grape-swagger-entity - grape_devise! grape_logging grape_url_validator hamster - heroku-deflater + has_scope + hashie + heroku_rails_deflate + hiredis (~> 0.6.0) httparty i18n-js + jbuilder + json (>= 2.3.0) + kaminari lograge - mail_view - memcachier mini_magick nearest_time_zone - parallel + nokogiri (~> 1.13.11)! param_validation! - pg + pg (< 1) + premailer-rails + protected_attributes pry - puma + pry-byebug + puma (~> 5.6) puma_worker_killer qx! rabl + rack! rack-attack + rack-cors + rack-freeze rack-timeout - rails (= 3.2.22.5) - rails-i18n (~> 3.0.0) + rails (~> 4.0) + rails-i18n rails_12factor + rails_autoscale_agent (>= 0.9.1) rake - roadie-rails - rspec - rspec-rails - ruby-debug-ide - ruby-prof (= 0.15.9) - sass (= 3.2.19) - sass-rails (= 3.2.6) + recaptcha (~> 5.8.1) + redis (>= 3.2.0) + redis-actionpack + rest-client + rspec (~> 3) + rspec-json_expectations + rspec-rails (~> 4) + ruby2_keywords + sassc + sassc-rails + securerandom + shoulda-matchers simplecov (~> 0.16.1) - sprockets - stripe - stripe-ruby-mock (~> 2.4.1)! + stripe (~> 4) + stripe-ruby-mock (~> 2.5.1) table_print - test-unit (~> 3.0) + test-unit timecop traceroute - uglifier + tunemygc webmock + yard RUBY VERSION - ruby 2.3.7p456 + ruby 2.6.10p210 BUNDLED WITH 1.17.3 diff --git a/Procfile b/Procfile index d6f090039..948e54291 100644 --- a/Procfile +++ b/Procfile @@ -1,3 +1,3 @@ web: bundle exec puma -C ./config/puma.rb worker: bundle exec rake jobs:work - +full_contact_worker: bundle exec rake work_full_contact_queue diff --git a/Procfile.dev b/Procfile.dev new file mode 100644 index 000000000..3c19bca79 --- /dev/null +++ b/Procfile.dev @@ -0,0 +1,3 @@ +web: bin/rails server -p $PORT +worker: bin/rake jobs:work +webpack: yarn watch diff --git a/README.md b/README.md index 72937dcda..bc62d74b2 100644 --- a/README.md +++ b/README.md @@ -1,159 +1,177 @@ -[![](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) +# CommitChange's version of Houdini -The Houdini Project is free and open source fundraising infrastructure. It includes... -- Crowdfunding campaigns -- Donate widget page and generator -- Fundraising events -- Nonprofit Profiles -- Nonprofit payment history and payouts dashboard -- Nonprofit recurring donation management dashboard -- Nonprofit metrics overview / business intelligence dashboard -- Nonprofit supporter relationship management dashboard (CRM) -- Nonprofit org user account management -- Simple donation management for donors +This is a Rails 4.2 app. -This is a Rails 3.2 app; [we want to upgrade](https://github.com/houdiniproject/houdini/issues/47). - -Much of the business logic is in `/lib`. - -The frontend is written in a few custom frameworks, the largest of which is called Flimflam. +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 backend code and React components should be well-tested + -## Get involved -Houdini's success depends on you! +## Prerequisites -### Join our Zulip chat -https://houdini.zulipchat.com +Houdini is designed and tested to run with the following: + +* Ruby 2.6 +* Node 14 +* PostgreSQL 12 +* run on Heroku-20 -### Help with translations -Visit the Internationalization channel on Houdini Zulip and discuss ## Dev Setup -#### Get the code -`git clone https://github.com/HoudiniProject/houdini` +#### Get the code +```bash +git clone https://github.com/Commitchange/houdini +git checkout supporter_level_goal +``` + +#### One-time setup (Ubuntu) -#### 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. +You'll want to run the next commands as root or via sudo (for Ubuntu 18.04 users or anyone running ProgresSQL 10, change "postgresql-12" below to "postgresql-10"). You could do this by typing `sudo /bin/sh` running the commands from there. -* *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. +```bash +apt update +apt install curl -yy +curl -sL https://deb.nodesource.com/setup_14.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. -##### Add yourself to the docker group -Adding yourself as a Docker group user as follows: +> *Note*: We use [rbenv](https://github.com/rbenv/rbenv) to have more control over the exact version of +> Ruby. This tool is useful for switching between multiple Ruby versions on the same machine and for +> ensuring that each project you are working on always runs on the correct Ruby version. You could also +> build ruby from source. -`sudo usermod -aG docker $USER` +> *Note*: 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. -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** +> *Tip*: To get out of the root shell, run `exit` + +Get the latest rbenv +```bash +git clone https://github.com/rbenv/rbenv.git ~/.rbenv ``` -./dc build -./dc up +Add rbenv to bashrc: +```bash +echo 'eval "$(~/.rbenv/bin/rbenv init - bash)"' >> ~/.bashrc ``` -#### 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` +> *Note:* close and reopen your terminal. -##### In console 2, , run the following and copy the output to you .env file to set you `SECRET_TOKEN` environment variable. +Download the rbenv install feature: +```bash +git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build ``` -./run rake secret # copy this result into your SECRET_TOKEN +Ruby install +```bash +cd houdini +rbenv install 2.6 ``` -##### 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 +Run the following command as the `postgres` user and then enter your admin +password at the prompt. -##### 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. +> *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. -##### In console 2, install npm packages -`./run npm install` +> *Tip*: To run this, add `sudo -u postgres ` to the beginning of the following command. -##### In console 2, fill the db -`./run rake db:create db:structure:load db:seed test:prepare` +`createuser admin -s -d -P` -##### 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. +#### One-time setup (Mac) -#### Startup -##### Switch back to console 1 and run `Ctrl-c` to end the session. +Set your Ruby version with `rbenv`. + +```bash +brew install rbenv +rbenv versions # see which ruby versions are already installed +rbenv install # the app currently uses version 2.6.10 +rbenv local # rbenv local --unset reverses the action + +# To switch between rbenv versions installed locally, use the following command: +rbenv shell 2.6.10 + +``` + +Set your Node version with `NVM`. -##### In console 1, restart the containers -`./dc up` +```bash +brew install nvm +brew info nvm # command that shows the remaining steps to complete to install nvm properly +mkdir ~/.nvm +nvm install 14 +nvm use 14 +# Add the following lines to your ~/.bashprofile or ~/.zshrc: +echo 'export NVM_DIR="$HOME/.nvm"' >> ~/.zshrc +echo '[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm' >> ~/.zshrc +echo '[ -s "$NVM_DIR/bash_completion" ] && \. "$NVM_DIR/bash_completion" # This loads nvm bash_completion' >> ~/.zshrc -##### In console 2, run: -`./run npm run watch` +# Reference Stack Overflow post: https://stackoverflow.com/questions/53118850/brew-install-nvm-nvm-command-not-found +``` -##### You can go to http://localhost:5000 +Set your Postgres version with homebrew. -To get started, register your nonprofit using the "Get Started" link. +```bash +brew install postgresql@12 +brew switch postgres@12 -## Additional info +# To start postgres locally run: +brew services start postgresql@12 -##### Super admin -There is a way to set your user as a super_admin. This role lets you access any of the nonprofits -on your Houdini instance. Additionally, it gives you access to the super admin control panel to search all supporters and -nonprofits, which is located at `/admin` url. - -To create the super user, go to the rails console by calling: +``` -`./dc run web rails console` +Create necessary postgres users in the `psql` console. -In the console, run the following: - +```bash +psql postgres # if this doesn't work, make sure postgres is running +CREATE ROLE admin WITH SUPERUSER CREATEDB LOGIN PASSWORD 'password'; +CREATE ROLE postgres WITH SUPERUSER CREATEDB LOGIN PASSWORD 'password'; ``` -admin=User.find(1) #or the id of the user you want to add the role -role=Role.create(user:admin,name: "super_admin") + +#### System configuration (all) +There are a number of steps for configuring your Houdini instance for startup +##### Run bin/setup +```sh +bin/setup ``` +##### Get your .env file +If you don't already have access to the CommitChange 1Password vault, ask to be added. Then +download the .env file in 1Password and place it in the root directory. -## To run in production +> *Note:* Double check that your .env file has the '.' in front of the file name. -##### 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! +#### Startup +##### run foreman for development -(To be continued) -- rake assets:precompile -- if production: make sure memcached is running. +When you run foreman in dev, you start up the server, the job runner and webpack. +```sh +foreman start +``` +If you get `ActiveRecord::NoDatabaseError` errors, run `bin/rake db:create:all` to make sure all the databases are built. ## 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: +## Documentation -### 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. +You can get generate documentation for the Ruby source by running: -### react:component -This generator creates a React component along with a test file for testing with Jest. -Each component should have its own file. +`bundle exec yard doc` -### react:lib -This generator creates a basic Typescript module along with a test file. +Alternatively, you can have it run in local webserver and autoupdate by running: + +`bundle exec yard server -r` ### Providing the complete corresponding source code @@ -179,7 +197,7 @@ For this to work though, the following characteristics must be true: - 2 spaces for tabs #### New frontend code -- All new front end code should be written in Typescript +- 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 @@ -191,3 +209,88 @@ and React (using TSX files). Please use the React Generators for creation. #### Git - No need to rebase, just merge + + +## How to build releases at CommitChange + +### Build for production + +* Make your changes on `supporter_level_goal` (or any branch in the public houdini repo) and commit +* Push your changes to remote +* Run `./create_new_release.sh`. This moves you to `PRIVATE_MASTER` (ask Eric for the remote and access) and merges the changes. +* Push to remote for `PRIVATE_MASTER` +* Checkout `PRIVATE_PROD_DEPLOY` +*`git merge PRIVATE_PROD_MASTER` +* If you have changes on assets or on javascript, then run: `./run_production yarn build-all`. After that finishes, run `git add public` and then `git commit` +* If no changes on assets or javascript, don’t do the last step +* Push to the remote for `PRIVATE_PROD_DEPLOY` (ask Eric for the remote and access) +* Push to heroku production using `git commit production PRIVATE_PROD_DEPLOY:master` ( ask Eric for access to `production`) + +## (Mac Setup) Build for Production +# In order to get prod env set, you need to download Github CLI and Heroku CLI. + +# Github CLI setup +``` +gh # to check if GH CLI has been downloaded +brew install gh # if GH CLI has not been downloaded +gh +gh auth +gh auth login +``` +# Answer the following questions: +—? What account do you want to log into? GitHub.com +? What is your preferred protocol for Git operations? HTTPS +? Authenticate Git with your GitHub credentials? Yes +? How would you like to authenticate GitHub CLI? Login with a web browser +—copy one time code, and enter into browser + + +# Heroku CLI setup +``` +brew tap heroku/brew && brew install heroku +heroku login +heroku git:remote —remote=production -a commitchange +git branch +git push production HEAD:master +``` + +# One-time setup to build for production +``` +git checkout supporter_level_goal +git pull +git remote add private https://github.com/commitchange/deploy-houdini.git +git branch -u private/master PRIVATE_MASTER +git fetch private +git checkout private/master +git switch -c PRIVATE_MASTER +git branch -u private/master PRIVATE_MASTER +git checkout private/prod_deploy +git switch -c PRIVATE_PROD_DEPLOY +git branch -u private/prod_deploy PRIVATE_PROD_DEPLOY +``` + +``` +git checkout supporter_level_goal +./create_new_release.sh +git push private HEAD:master +git checkout PRIVATE_PROD_DEPLOY +git merge PRIVATE_MASTER +git push private HEAD:prod_deploy +npm run build-all-production +git add public +git commit -m "" +git push private HEAD:prod_deploy +git push production HEAD:master +``` +### Build for staging + +* Run the workflow at https://github.com/CommitChange/deploy-houdini/actions/workflows/create-release.yml. +* Once the deploy finishes, increase ASSET_VERSION in https://dashboard.heroku.com/apps/commitchange-test/settings by 1 + +## Creating issues + +* *I'm a community member* - You should file an issue upstream in https://github.com/houdiniproject/houdini + +* *I work for CommitChange and...* + * *this is a CommitChange issue* - create an issue in https://github.com/commitchange/tix + * *this may be an issue upstream* - create an issue in https://github.com/commitchange/tix and maybe upstream. If you do file upstream, link to it the upstream issue in the tix issue. diff --git a/app/api/houdini.rb b/app/api/houdini.rb new file mode 100644 index 000000000..70110a238 --- /dev/null +++ b/app/api/houdini.rb @@ -0,0 +1,2 @@ +module Houdini +end \ No newline at end of file diff --git a/app/api/houdini/v1/api.rb b/app/api/houdini/v1/api.rb index d0e3a9216..642b7cb22 100644 --- a/app/api/houdini/v1/api.rb +++ b/app/api/houdini/v1/api.rb @@ -14,6 +14,7 @@ class Houdini::V1::API < Grape::API 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}" : ""}", diff --git a/app/api/houdini/v1/helpers/application_helper.rb b/app/api/houdini/v1/helpers/application_helper.rb index 0c1345d61..194be8677 100644 --- a/app/api/houdini/v1/helpers/application_helper.rb +++ b/app/api/houdini/v1/helpers/application_helper.rb @@ -28,18 +28,5 @@ def protect_against_forgery? allow_forgery_protection.nil? || allow_forgery_protection end - - # 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.new(output) - # - # end - # end - end diff --git a/app/api/houdini/v1/nonprofit.rb b/app/api/houdini/v1/nonprofit.rb index e01bc7cc5..665386946 100644 --- a/app/api/houdini/v1/nonprofit.rb +++ b/app/api/houdini/v1/nonprofit.rb @@ -3,7 +3,6 @@ class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI helpers Houdini::V1::Helpers::ApplicationHelper, Houdini::V1::Helpers::RescueHelper before do - protect_against_forgery end desc 'Return a nonprofit.' do @@ -59,14 +58,14 @@ class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI u = nil Qx.transaction do begin - np = Nonprofit.new(OnboardAccounts.set_nonprofit_defaults(declared_params[:nonprofit])) + 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) + slug = ::SlugNonprofitNamingAlgorithm.new(np.state_code_slug, np.city_slug).create_copy_name(np.slug) np.slug = slug np.save! rescue UnableToCreateNameCopyError @@ -81,15 +80,21 @@ class Houdini::V1::Nonprofit < Houdini::V1::BaseAPI end end - u = User.new(declared_params[:user]) + 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) + + MailchimpNonprofitUserAddJob.perform_later( u, np) + + billing_plan = ::BillingPlan.find(Settings.default_bp.id) b_sub = np.build_billing_subscription(billing_plan: billing_plan, status: 'active') b_sub.save! + ::StripeAccountUtils.find_or_create(np.id) + np.reload + + ::Delayed::Job.enqueue ::JobTypes::NonprofitCreateJob.new(np.id) rescue ActiveRecord::RecordInvalid => e class_to_name = {Nonprofit => 'nonprofit', User => 'user'} if class_to_name[e.record.class] diff --git a/app/assets/fonts/Bitter/Bitter-Bold.eot b/app/assets/fonts/Bitter/Bitter-Bold.eot deleted file mode 100644 index 533502da5..000000000 Binary files a/app/assets/fonts/Bitter/Bitter-Bold.eot and /dev/null differ diff --git a/app/assets/fonts/Bitter/Bitter-Bold.svg b/app/assets/fonts/Bitter/Bitter-Bold.svg deleted file mode 100644 index ec5b943fd..000000000 --- a/app/assets/fonts/Bitter/Bitter-Bold.svg +++ /dev/null @@ -1,248 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 1390173b3..000000000 Binary files a/app/assets/fonts/Bitter/Bitter-Bold.ttf and /dev/null differ diff --git a/app/assets/fonts/Bitter/Bitter-Regular.eot b/app/assets/fonts/Bitter/Bitter-Regular.eot deleted file mode 100644 index 46a2fc129..000000000 Binary files a/app/assets/fonts/Bitter/Bitter-Regular.eot and /dev/null differ diff --git a/app/assets/fonts/Bitter/Bitter-Regular.svg b/app/assets/fonts/Bitter/Bitter-Regular.svg deleted file mode 100644 index 8293c82f8..000000000 --- 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 3b66905ad..000000000 Binary files a/app/assets/fonts/Bitter/Bitter-Regular.ttf and /dev/null differ diff --git a/app/assets/fonts/FontAwesome/fontawesome-webfont.eot b/app/assets/fonts/FontAwesome/fontawesome-webfont.eot deleted file mode 100755 index 6cfd56609..000000000 Binary files a/app/assets/fonts/FontAwesome/fontawesome-webfont.eot and /dev/null differ diff --git a/app/assets/fonts/FontAwesome/fontawesome-webfont.svg b/app/assets/fonts/FontAwesome/fontawesome-webfont.svg deleted file mode 100755 index a9f846950..000000000 --- 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 5cd6cff6d..000000000 Binary files a/app/assets/fonts/FontAwesome/fontawesome-webfont.ttf and /dev/null differ diff --git a/app/assets/fonts/Open_Sans/opensans-bold-webfont.eot b/app/assets/fonts/Open_Sans/opensans-bold-webfont.eot deleted file mode 100644 index e23a5d3af..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-bold-webfont.eot and /dev/null differ 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 063afd04f..000000000 --- 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 711174458..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-bold-webfont.ttf and /dev/null differ 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 77dc71d6d..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-light-webfont.eot and /dev/null differ diff --git a/app/assets/fonts/Open_Sans/opensans-light-webfont.svg b/app/assets/fonts/Open_Sans/opensans-light-webfont.svg deleted file mode 100644 index 2419d92fb..000000000 --- a/app/assets/fonts/Open_Sans/opensans-light-webfont.svg +++ /dev/null @@ -1,1825 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 209e05248..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-light-webfont.ttf and /dev/null differ 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 17e309450..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-regular-webfont.eot and /dev/null differ diff --git a/app/assets/fonts/Open_Sans/opensans-regular-webfont.svg b/app/assets/fonts/Open_Sans/opensans-regular-webfont.svg deleted file mode 100644 index 800a03a34..000000000 --- a/app/assets/fonts/Open_Sans/opensans-regular-webfont.svg +++ /dev/null @@ -1,1825 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ 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 c6413c2a7..000000000 Binary files a/app/assets/fonts/Open_Sans/opensans-regular-webfont.ttf and /dev/null differ 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 781275130..000000000 Binary files a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.eot and /dev/null differ 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 a65a2fd3d..000000000 --- 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 384a91c99..000000000 Binary files a/app/assets/fonts/Open_Sans_Condensed/opensans-condbold-webfont.ttf and /dev/null differ diff --git a/app/assets/fonts/Streamline/streamline-30px.eot b/app/assets/fonts/Streamline/streamline-30px.eot deleted file mode 100644 index a16a50ce9..000000000 Binary files a/app/assets/fonts/Streamline/streamline-30px.eot and /dev/null differ diff --git a/app/assets/fonts/Streamline/streamline-30px.svg b/app/assets/fonts/Streamline/streamline-30px.svg deleted file mode 100644 index d457189c3..000000000 --- a/app/assets/fonts/Streamline/streamline-30px.svg +++ /dev/null @@ -1,1652 +0,0 @@ - - - -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 c5375dedb..000000000 Binary files a/app/assets/fonts/Streamline/streamline-30px.ttf and /dev/null differ diff --git a/app/assets/images/favicon.ico b/app/assets/images/favicon.ico new file mode 100755 index 000000000..24bf09f7b Binary files /dev/null and b/app/assets/images/favicon.ico differ diff --git a/app/assets/images/graphics/half-circle-bottom.svg b/app/assets/images/graphics/half-circle-bottom.svg index 35c349919..89a629e2e 100644 --- a/app/assets/images/graphics/half-circle-bottom.svg +++ b/app/assets/images/graphics/half-circle-bottom.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/half-circle-left.svg b/app/assets/images/graphics/half-circle-left.svg index 99ddd772e..28a1c8023 100644 --- a/app/assets/images/graphics/half-circle-left.svg +++ b/app/assets/images/graphics/half-circle-left.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/half-circle-right.svg b/app/assets/images/graphics/half-circle-right.svg index 5e5483f91..46d624713 100644 --- a/app/assets/images/graphics/half-circle-right.svg +++ b/app/assets/images/graphics/half-circle-right.svg @@ -1,18 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/half-circle-top.svg b/app/assets/images/graphics/half-circle-top.svg index 4473582a7..b3a7921d3 100644 --- a/app/assets/images/graphics/half-circle-top.svg +++ b/app/assets/images/graphics/half-circle-top.svg @@ -1,22 +1 @@ - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/icon-grip.svg b/app/assets/images/graphics/icon-grip.svg index 50ac0eafb..53ed6dcde 100644 --- a/app/assets/images/graphics/icon-grip.svg +++ b/app/assets/images/graphics/icon-grip.svg @@ -1,15 +1 @@ - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/one-time.svg b/app/assets/images/graphics/one-time.svg index 5e2c30334..52e628a4e 100644 --- a/app/assets/images/graphics/one-time.svg +++ b/app/assets/images/graphics/one-time.svg @@ -1,14 +1 @@ - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/graphics/recurring.svg b/app/assets/images/graphics/recurring.svg index 8185a76f2..e7fc820d1 100644 --- a/app/assets/images/graphics/recurring.svg +++ b/app/assets/images/graphics/recurring.svg @@ -1,25 +1 @@ - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/logos/commitchange-logo-rectangle-color-150.png b/app/assets/images/logos/commitchange-logo-rectangle-color-150.png new file mode 100644 index 000000000..5c32a2b13 Binary files /dev/null and b/app/assets/images/logos/commitchange-logo-rectangle-color-150.png differ diff --git a/app/assets/images/logos/commitchange_logo_bug.svg b/app/assets/images/logos/commitchange_logo_bug.svg new file mode 100644 index 000000000..1b2022495 --- /dev/null +++ b/app/assets/images/logos/commitchange_logo_bug.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logos/commitchange_logo_full.svg b/app/assets/images/logos/commitchange_logo_full.svg new file mode 100644 index 000000000..0eae27b89 --- /dev/null +++ b/app/assets/images/logos/commitchange_logo_full.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/assets/images/logos/houdini_project_border_64.svg b/app/assets/images/logos/houdini_project_border_64.svg index 106021d0b..6f93ac594 100644 --- a/app/assets/images/logos/houdini_project_border_64.svg +++ b/app/assets/images/logos/houdini_project_border_64.svg @@ -1,139 +1 @@ - - - -image/svg+xml - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/assets/images/logos/houdini_project_bug.svg b/app/assets/images/logos/houdini_project_bug.svg index 6ba6614c8..6ccb503b4 100644 --- a/app/assets/images/logos/houdini_project_bug.svg +++ b/app/assets/images/logos/houdini_project_bug.svg @@ -1,11 +1 @@ - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/logos/houdini_project_full.svg b/app/assets/images/logos/houdini_project_full.svg index 36c71d971..e99cbe66a 100644 --- a/app/assets/images/logos/houdini_project_full.svg +++ b/app/assets/images/logos/houdini_project_full.svg @@ -1,54 +1 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/images/ui_components/close.svg b/app/assets/images/ui_components/close.svg index 455e7061f..96ed15ca1 100644 --- a/app/assets/images/ui_components/close.svg +++ b/app/assets/images/ui_components/close.svg @@ -1,14 +1 @@ - - - - - - - - - - - - - + \ No newline at end of file diff --git a/app/assets/stylesheets/body.css.scss b/app/assets/stylesheets/body.scss similarity index 100% rename from app/assets/stylesheets/body.css.scss rename to app/assets/stylesheets/body.scss diff --git a/app/assets/stylesheets/boot/editor.css.scss.erb b/app/assets/stylesheets/boot/editor.css.scss.erb deleted file mode 100644 index bcf1e7c13..000000000 --- a/app/assets/stylesheets/boot/editor.css.scss.erb +++ /dev/null @@ -1,3 +0,0 @@ -<% # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> -@import 'common/vendor/froala_editor'; -@import 'common/vendor/quill.bubble'; diff --git a/app/assets/stylesheets/boot/editor.scss.erb b/app/assets/stylesheets/boot/editor.scss.erb new file mode 100644 index 000000000..b9828d28c --- /dev/null +++ b/app/assets/stylesheets/boot/editor.scss.erb @@ -0,0 +1,2 @@ +<% # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> +@import 'common/vendor/froala_editor'; diff --git a/app/assets/stylesheets/boot/font-awesome.css.scss.erb b/app/assets/stylesheets/boot/font-awesome.css.scss.erb deleted file mode 100644 index 85fbe4974..000000000 --- a/app/assets/stylesheets/boot/font-awesome.css.scss.erb +++ /dev/null @@ -1,1568 +0,0 @@ -<% # 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) - */ - -$path: "<%= asset_path('FontAwesome') %>"; - -@font-face { - font-family: 'FontAwesome'; - src: url($path + '/fontawesome-webfont.eot?v=4.1.0'); - src: url($path + '/fontawesome-webfont.eot?#iefix&v=4.1.0') format('embedded-opentype'), url($path + '/fontawesome-webfont.woff?v=4.1.0') format('woff'), url($path + '/fontawesome-webfont.ttf?v=4.1.0') format('truetype'), url($path + '/fontawesome-webfont.svg?v=4.1.0#fontawesomeregular') format('svg'); - 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: .2em .25em .15em; - border: solid 0.08em #eeeeee; - border-radius: .1em; -} -.pull-right { - float: right; -} -.pull-left { - float: left; -} -.fa.pull-left { - margin-right: .3em; -} -.fa.pull-right { - margin-left: .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/font-awesome.scss.erb b/app/assets/stylesheets/boot/font-awesome.scss.erb new file mode 100644 index 000000000..dee65784c --- /dev/null +++ b/app/assets/stylesheets/boot/font-awesome.scss.erb @@ -0,0 +1,1544 @@ +<% # 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) + */ + +$path: "/assets/FontAwesome"; + +@font-face { + font-family: 'FontAwesome'; + src: url('<%= asset_path("FontAwesome/fontawesome-webfont.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: .2em .25em .15em; + border: solid 0.08em #eeeeee; + border-radius: .1em; +} +.pull-right { + float: right; +} +.pull-left { + float: left; +} +.fa.pull-left { + margin-right: .3em; +} +.fa.pull-right { + margin-left: .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; +} + +@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.erb b/app/assets/stylesheets/boot/google-webfonts.css.scss.erb deleted file mode 100644 index 31ef30d05..000000000 --- a/app/assets/stylesheets/boot/google-webfonts.css.scss.erb +++ /dev/null @@ -1,77 +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: url('<%= asset_path('Open_Sans/opensans-regular-webfont.eot') %>'); - src: url('<%= asset_path('Open_Sans/opensans-regular-webfont.eot?#iefix') %>') format('embedded-opentype'), - url('<%= asset_path('Open_Sans/opensans-regular-webfont.woff') %>') format('woff'), - url('<%= asset_path('Open_Sans/opensans-regular-webfont.ttf') %>') format('truetype'), - url('<%= asset_path('Open_Sans/opensans-regular-webfont.svg#open_sansregular') %>') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Open Sans'; - src: url('<%= asset_path('Open_Sans/opensans-light-webfont.eot') %>'); - src: url('<%= asset_path('Open_Sans/opensans-light-webfont.eot?#iefix') %>') format('embedded-opentype'), - url('<%= asset_path('Open_Sans/opensans-light-webfont.woff') %>') format('woff'), - url('<%= asset_path('Open_Sans/opensans-light-webfont.ttf') %>') format('truetype'), - url('<%= asset_path('Open_Sans/opensans-light-webfont.svg#open_sanslight') %>') format('svg'); - font-weight: 200; - font-style: normal; -} - -@font-face { - font-family: 'Open Sans'; - src: url('<%= asset_path('Open_Sans/opensans-bold-webfont.eot') %>'); - src: url('<%= asset_path('Open_Sans/opensans-bold-webfont.eot?#iefix') %>') format('embedded-opentype'), - url('<%= asset_path('Open_Sans/opensans-bold-webfont.woff') %>') format('woff'), - url('<%= asset_path('Open_Sans/opensans-bold-webfont.ttf') %>') format('truetype'), - url('<%= asset_path('Open_Sans/opensans-bold-webfont.svg#open_sansbold') %>') format('svg'); - font-weight: bold; - font-style: normal; -} - - -/* Bitter */ - -$condensed: '<%= asset_path('Open_Sans_Condensed') %>'; - -@font-face { - font-family: 'OpenSansCondensed'; - src: url($condensed + '/opensans-condbold-webfont.eot'); - src: url($condensed + '/opensans-condbold-webfont.eot?#iefix') format('embedded-opentype'), - url($condensed + '/opensans-condbold-webfont.woff') format('woff'), - url($condensed + '/opensans-condbold-webfont.ttf') format('truetype'), - url($condensed + '/opensans-condbold-webfont.svg') format('svg'); - font-weight: normal; - font-style: normal; -} - - -@font-face { - font-family: 'Bitter'; - src: url('<%= asset_path('Bitter/Bitter-Regular.eot') %>'); - src: url('<%= asset_path('Bitter/Bitter-Regular.eot?#iefix') %>') format('embedded-opentype'), - url('<%= asset_path('Bitter/Bitter-Regular.woff') %>') format('woff'), - url('<%= asset_path('Bitter/Bitter-Regular.ttf') %>') format('truetype'), - url('<%= asset_path('Bitter/Bitter-Regular.svg#bitterregular') %>') format('svg'); - font-weight: normal; - font-style: normal; -} - -@font-face { - font-family: 'Bitter'; - src: url('<%= asset_path('Bitter/Bitter-Bold.eot') %>'); - src: url('<%= asset_path('Bitter/Bitter-Bold.eot?#iefix') %>') format('embedded-opentype'), - url('<%= asset_path('Bitter/Bitter-Bold.woff') %>') format('woff'), - url('<%= asset_path('Bitter/Bitter-Bold.ttf') %>') format('truetype'), - url('<%= asset_path('Bitter/Bitter-Bold.svg#bitterbold') %>') format('svg'); - font-weight: bold; - font-style: normal; -} - - diff --git a/app/assets/stylesheets/boot/google-webfonts.scss.erb b/app/assets/stylesheets/boot/google-webfonts.scss.erb new file mode 100644 index 000000000..7d409cbe5 --- /dev/null +++ b/app/assets/stylesheets/boot/google-webfonts.scss.erb @@ -0,0 +1,54 @@ +<% # 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: url('<%= asset_path("Open_Sans/opensans-regular-webfont.woff") %>'); + + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('<%= asset_path("Open_Sans/opensans-light-webfont.woff") %>'); + font-weight: 200; + font-style: normal; +} + +@font-face { + font-family: 'Open Sans'; + src: url('<%= asset_path("Open_Sans/opensans-bold-webfont.woff") %>'); + font-weight: bold; + font-style: normal; +} + + +/* Bitter */ + + + +@font-face { + font-family: 'OpenSansCondensed'; + src: url('<%= asset_path("Open_Sans_Condensed/opensans-condbold-webfont.woff") %>'); + font-weight: normal; + font-style: normal; +} + + +@font-face { + font-family: 'Bitter'; + src: url('<%= asset_path("Bitter/Bitter-Regular.woff") %>'); + font-weight: normal; + font-style: normal; +} + +@font-face { + font-family: 'Bitter'; + src: url('<%= asset_path("Bitter/Bitter-Bold.woff") %>'); + font-weight: bold; + font-style: normal; +} + + diff --git a/app/assets/stylesheets/boot/streamline-icons.css.scss.erb b/app/assets/stylesheets/boot/streamline-icons.css.scss.erb deleted file mode 100644 index 36a5ea78f..000000000 --- a/app/assets/stylesheets/boot/streamline-icons.css.scss.erb +++ /dev/null @@ -1,4968 +0,0 @@ -<% # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> -@charset "UTF-8"; - -$path: "<%= asset_path('Streamline') %>"; - -@font-face { - font-family: "streamline-30px"; - src:url($path + "/streamline-30px.eot"); - src:url($path + "/streamline-30px.eot?#iefix") format("embedded-opentype"), - url($path + "/streamline-30px.woff") format("woff"), - url($path + "/streamline-30px.ttf") format("truetype"), - url($path + "/streamline-30px.svg#streamline-30px") format("svg"); - font-weight: normal; - font-style: normal; -} - -[data-icon]:before { - font-family: "streamline-30px" !important; - content: attr(data-icon); - font-style: normal !important; - font-weight: normal !important; - font-variant: normal !important; - text-transform: none !important; - speak: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -[class^="icon-"]:before, -[class*=" icon-"]:before { - font-family: "streamline-30px" !important; - font-style: normal !important; - font-weight: normal !important; - font-variant: normal !important; - text-transform: none !important; - speak: none; - line-height: 1; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -.icon-aim-1:before { - content: "\e000"; -} -.icon-aim-2:before { - content: "\e001"; -} -.icon-aim-3:before { - content: "\e002"; -} -.icon-bin-1:before { - content: "\e003"; -} -.icon-bin-2:before { - content: "\e004"; -} -.icon-binocular:before { - content: "\e005"; -} -.icon-bomb:before { - content: "\e006"; -} -.icon-clip-1:before { - content: "\e007"; -} -.icon-clip-2:before { - content: "\e008"; -} -.icon-cutter:before { - content: "\e009"; -} -.icon-delete-1:before { - content: "\e00a"; -} -.icon-delete-2:before { - content: "\e00b"; -} -.icon-edit-1:before { - content: "\e00c"; -} -.icon-edit-2:before { - content: "\e00d"; -} -.icon-edit-3:before { - content: "\e00e"; -} -.icon-hide:before { - content: "\e00f"; -} -.icon-ink:before { - content: "\e010"; -} -.icon-key-1:before { - content: "\e011"; -} -.icon-key-2:before { - content: "\e012"; -} -.icon-link-1:before { - content: "\e013"; -} -.icon-link-2:before { - content: "\e014"; -} -.icon-link-3:before { - content: "\e015"; -} -.icon-link-broken-1:before { - content: "\e016"; -} -.icon-link-broken-2:before { - content: "\e017"; -} -.icon-lock-1:before { - content: "\e018"; -} -.icon-lock-2:before { - content: "\e019"; -} -.icon-lock-3:before { - content: "\e01a"; -} -.icon-lock-4:before { - content: "\e01b"; -} -.icon-lock-5:before { - content: "\e01c"; -} -.icon-lock-unlock-1:before { - content: "\e01d"; -} -.icon-lock-unlock-2:before { - content: "\e01e"; -} -.icon-magnifier:before { - content: "\e01f"; -} -.icon-pen-1:before { - content: "\e020"; -} -.icon-pen-2:before { - content: "\e021"; -} -.icon-pen-3:before { - content: "\e022"; -} -.icon-pen-4:before { - content: "\e023"; -} -.icon-pencil-1:before { - content: "\e024"; -} -.icon-pencil-2:before { - content: "\e025"; -} -.icon-pencil-3:before { - content: "\e026"; -} -.icon-pin-1:before { - content: "\e027"; -} -.icon-pin-2:before { - content: "\e028"; -} -.icon-power-1:before { - content: "\e029"; -} -.icon-power-2:before { - content: "\e02a"; -} -.icon-preview-1:before { - content: "\e02b"; -} -.icon-preview-2:before { - content: "\e02c"; -} -.icon-scissor-1:before { - content: "\e02d"; -} -.icon-scissor-2:before { - content: "\e02e"; -} -.icon-skull-1:before { - content: "\e02f"; -} -.icon-skull-2:before { - content: "\e030"; -} -.icon-type-1:before { - content: "\e031"; -} -.icon-type-2:before { - content: "\e032"; -} -.icon-type-3:before { - content: "\e033"; -} -.icon-type-4:before { - content: "\e034"; -} -.icon-zoom-area:before { - content: "\e035"; -} -.icon-zoom-in:before { - content: "\e036"; -} -.icon-zoom-out:before { - content: "\e037"; -} -.icon-cursor-1:before { - content: "\e038"; -} -.icon-cursor-2:before { - content: "\e039"; -} -.icon-cursor-3:before { - content: "\e03a"; -} -.icon-cursor-6:before { - content: "\e03b"; -} -.icon-cursor-move:before { - content: "\e03c"; -} -.icon-cursor-select-area:before { - content: "\e03d"; -} -.icon-cursors:before { - content: "\e03e"; -} -.icon-hand:before { - content: "\e03f"; -} -.icon-hand-block:before { - content: "\e040"; -} -.icon-hand-grab-1:before { - content: "\e041"; -} -.icon-hand-grab-2:before { - content: "\e042"; -} -.icon-hand-point:before { - content: "\e043"; -} -.icon-hand-touch-1:before { - content: "\e044"; -} -.icon-hand-touch-2:before { - content: "\e045"; -} -.icon-hand-touch-3:before { - content: "\e046"; -} -.icon-hand-touch-4:before { - content: "\e047"; -} -.icon-bookmark-1:before { - content: "\e048"; -} -.icon-bookmark-2:before { - content: "\e049"; -} -.icon-bookmark-3:before { - content: "\e04a"; -} -.icon-bookmark-4:before { - content: "\e04b"; -} -.icon-tag-1:before { - content: "\e04c"; -} -.icon-tag-2:before { - content: "\e04d"; -} -.icon-tag-add:before { - content: "\e04e"; -} -.icon-tag-delete:before { - content: "\e04f"; -} -.icon-tags-1:before { - content: "\e050"; -} -.icon-tags-2:before { - content: "\e051"; -} -.icon-anchor-point-1:before { - content: "\e052"; -} -.icon-anchor-point-2:before { - content: "\e053"; -} -.icon-arrange-1:before { - content: "\e054"; -} -.icon-arrange-2:before { - content: "\e055"; -} -.icon-board:before { - content: "\e056"; -} -.icon-brush-1:before { - content: "\e057"; -} -.icon-brush-2:before { - content: "\e058"; -} -.icon-bucket:before { - content: "\e059"; -} -.icon-crop:before { - content: "\e05a"; -} -.icon-dropper-1:before { - content: "\e05b"; -} -.icon-dropper-2:before { - content: "\e05c"; -} -.icon-dropper-3:before { - content: "\e05d"; -} -.icon-glue:before { - content: "\e05e"; -} -.icon-grid:before { - content: "\e05f"; -} -.icon-layers:before { - content: "\e060"; -} -.icon-magic-wand-1:before { - content: "\e061"; -} -.icon-magic-wand-2:before { - content: "\e062"; -} -.icon-magnet:before { - content: "\e063"; -} -.icon-marker:before { - content: "\e064"; -} -.icon-palette:before { - content: "\e065"; -} -.icon-pen-5:before { - content: "\e066"; -} -.icon-pen-6:before { - content: "\e067"; -} -.icon-quill:before { - content: "\e068"; -} -.icon-reflect:before { - content: "\e069"; -} -.icon-roller:before { - content: "\e06a"; -} -.icon-ruler-1:before { - content: "\e06b"; -} -.icon-ruler-2:before { - content: "\e06c"; -} -.icon-scale-diagonal-1:before { - content: "\e06d"; -} -.icon-scale-diagonal-2:before { - content: "\e06e"; -} -.icon-scale-horizontal:before { - content: "\e06f"; -} -.icon-scale-tool-1:before { - content: "\e070"; -} -.icon-scale-tool-2:before { - content: "\e071"; -} -.icon-scale-tool-3:before { - content: "\e072"; -} -.icon-scale-vertical:before { - content: "\e073"; -} -.icon-shear-tool:before { - content: "\e074"; -} -.icon-spray:before { - content: "\e075"; -} -.icon-stamp:before { - content: "\e076"; -} -.icon-stationery-1:before { - content: "\e077"; -} -.icon-stationery-2:before { - content: "\e078"; -} -.icon-stationery-3:before { - content: "\e079"; -} -.icon-vector:before { - content: "\e07a"; -} -.icon-award-1:before { - content: "\e07b"; -} -.icon-award-2:before { - content: "\e07c"; -} -.icon-award-3:before { - content: "\e07d"; -} -.icon-award-4:before { - content: "\e07e"; -} -.icon-award-5:before { - content: "\e07f"; -} -.icon-award-6:before { - content: "\e080"; -} -.icon-crown-1:before { - content: "\e081"; -} -.icon-crown-2:before { - content: "\e082"; -} -.icon-crown-3:before { - content: "\e083"; -} -.icon-fire:before { - content: "\e084"; -} -.icon-flag-1:before { - content: "\e085"; -} -.icon-flag-2:before { - content: "\e086"; -} -.icon-flag-3:before { - content: "\e087"; -} -.icon-flag-4:before { - content: "\e088"; -} -.icon-flag-5:before { - content: "\e089"; -} -.icon-flag-6:before { - content: "\e08a"; -} -.icon-flag-7:before { - content: "\e08b"; -} -.icon-flag-8:before { - content: "\e08c"; -} -.icon-google-plus-1:before { - content: "\e08d"; -} -.icon-google-plus-2:before { - content: "\e08e"; -} -.icon-hand-like-1:before { - content: "\e08f"; -} -.icon-hand-like-2:before { - content: "\e090"; -} -.icon-hand-unlike-1:before { - content: "\e091"; -} -.icon-hand-unlike-2:before { - content: "\e092"; -} -.icon-heart-1:before { - content: "\e093"; -} -.icon-heart-2:before { - content: "\e094"; -} -.icon-heart-angel:before { - content: "\e095"; -} -.icon-heart-broken:before { - content: "\e096"; -} -.icon-heart-minus:before { - content: "\e097"; -} -.icon-heart-plus:before { - content: "\e098"; -} -.icon-present:before { - content: "\e099"; -} -.icon-rank-1:before { - content: "\e09a"; -} -.icon-rank-2:before { - content: "\e09b"; -} -.icon-ribbon:before { - content: "\e09c"; -} -.icon-star-1:before { - content: "\e09d"; -} -.icon-star-2:before { - content: "\e09e"; -} -.icon-star-3:before { - content: "\e09f"; -} -.icon-star-4:before { - content: "\e0a0"; -} -.icon-star-5:before { - content: "\e0a1"; -} -.icon-star-6:before { - content: "\e0a2"; -} -.icon-star-7:before { - content: "\e0a3"; -} -.icon-star-8:before { - content: "\e0a4"; -} -.icon-star-9:before { - content: "\e0a5"; -} -.icon-star-10:before { - content: "\e0a6"; -} -.icon-trophy:before { - content: "\e0a7"; -} -.icon-baloon:before { - content: "\e0a8"; -} -.icon-bubble-1:before { - content: "\e0a9"; -} -.icon-bubble-2:before { - content: "\e0aa"; -} -.icon-bubble-add-1:before { - content: "\e0ab"; -} -.icon-bubble-add-2:before { - content: "\e0ac"; -} -.icon-bubble-add-3:before { - content: "\e0ad"; -} -.icon-bubble-ask-1:before { - content: "\e0ae"; -} -.icon-bubble-ask-2:before { - content: "\e0af"; -} -.icon-bubble-attention-2:before { - content: "\e0b0"; -} -.icon-bubble-attention-3:before { - content: "\e0b1"; -} -.icon-bubble-attention-4:before { - content: "\e0b2"; -} -.icon-bubble-attention-6:before { - content: "\e0b3"; -} -.icon-bubble-attention-7:before { - content: "\e0b4"; -} -.icon-bubble-block-1:before { - content: "\e0b5"; -} -.icon-bubble-block-2:before { - content: "\e0b6"; -} -.icon-bubble-block-3:before { - content: "\e0b7"; -} -.icon-bubble-chat-1:before { - content: "\e0b8"; -} -.icon-bubble-chat-2:before { - content: "\e0b9"; -} -.icon-bubble-check-1:before { - content: "\e0ba"; -} -.icon-bubble-check-2:before { - content: "\e0bb"; -} -.icon-bubble-check-3:before { - content: "\e0bc"; -} -.icon-bubble-comment-1:before { - content: "\e0bd"; -} -.icon-bubble-comment-2:before { - content: "\e0be"; -} -.icon-bubble-conversation-1:before { - content: "\e0bf"; -} -.icon-bubble-conversation-2:before { - content: "\e0c0"; -} -.icon-bubble-conversation-3:before { - content: "\e0c1"; -} -.icon-bubble-conversation-4:before { - content: "\e0c2"; -} -.icon-bubble-conversation-5:before { - content: "\e0c3"; -} -.icon-bubble-conversation-6:before { - content: "\e0c4"; -} -.icon-bubble-delete-1:before { - content: "\e0c5"; -} -.icon-bubble-delete-2:before { - content: "\e0c6"; -} -.icon-bubble-delete-3:before { - content: "\e0c7"; -} -.icon-bubble-edit-1:before { - content: "\e0c8"; -} -.icon-bubble-edit-2:before { - content: "\e0c9"; -} -.icon-bubble-edit-3:before { - content: "\e0ca"; -} -.icon-bubble-heart-1:before { - content: "\e0cb"; -} -.icon-bubble-heart-2:before { - content: "\e0cc"; -} -.icon-bubble-information:before { - content: "\e0cd"; -} -.icon-bubble-information-1:before { - content: "\e0ce"; -} -.icon-bubble-minus-1:before { - content: "\e0cf"; -} -.icon-bubble-minus-2:before { - content: "\e0d0"; -} -.icon-bubble-minus-3:before { - content: "\e0d1"; -} -.icon-bubble-quote-1:before { - content: "\e0d2"; -} -.icon-bubble-quote-2:before { - content: "\e0d3"; -} -.icon-bubble-smiley-1:before { - content: "\e0d4"; -} -.icon-bubble-smiley-2:before { - content: "\e0d5"; -} -.icon-bubble-smiley-3:before { - content: "\e0d6"; -} -.icon-bubble-smiley-4:before { - content: "\e0d7"; -} -.icon-bubble-star-1:before { - content: "\e0d8"; -} -.icon-bubble-star-2:before { - content: "\e0d9"; -} -.icon-bubble-star-3:before { - content: "\e0da"; -} -.icon-chat-1:before { - content: "\e0db"; -} -.icon-chat-2:before { - content: "\e0dc"; -} -.icon-chat-3:before { - content: "\e0dd"; -} -.icon-chat-4:before { - content: "\e0de"; -} -.icon-chat-5:before { - content: "\e0df"; -} -.icon-chat-6:before { - content: "\e0e0"; -} -.icon-chat-7:before { - content: "\e0e1"; -} -.icon-smiley-happy-1:before { - content: "\e0e2"; -} -.icon-smiley-happy-2:before { - content: "\e0e3"; -} -.icon-smiley-happy-3:before { - content: "\e0e4"; -} -.icon-smiley-happy-4:before { - content: "\e0e5"; -} -.icon-smiley-happy-5:before { - content: "\e0e6"; -} -.icon-smiley-sad-1:before { - content: "\e0e7"; -} -.icon-smiley-surprise:before { - content: "\e0e8"; -} -.icon-smiley-wink:before { - content: "\e0e9"; -} -.icon-call-1:before { - content: "\e0ea"; -} -.icon-call-2:before { - content: "\e0eb"; -} -.icon-call-3:before { - content: "\e0ec"; -} -.icon-call-4:before { - content: "\e0ed"; -} -.icon-call-add:before { - content: "\e0ee"; -} -.icon-call-block:before { - content: "\e0ef"; -} -.icon-call-delete:before { - content: "\e0f0"; -} -.icon-call-in:before { - content: "\e0f1"; -} -.icon-call-minus:before { - content: "\e0f2"; -} -.icon-call-out:before { - content: "\e0f3"; -} -.icon-contact:before { - content: "\e0f4"; -} -.icon-fax:before { - content: "\e0f5"; -} -.icon-hang-up:before { - content: "\e0f6"; -} -.icon-message:before { - content: "\e0f7"; -} -.icon-mobile-phone-1:before { - content: "\e0f8"; -} -.icon-mobile-phone-2:before { - content: "\e0f9"; -} -.icon-phone-1:before { - content: "\e0fa"; -} -.icon-phone-2:before { - content: "\e0fb"; -} -.icon-phone-3:before { - content: "\e0fc"; -} -.icon-phone-4:before { - content: "\e0fd"; -} -.icon-phone-vibration:before { - content: "\e0fe"; -} -.icon-signal-fine:before { - content: "\e0ff"; -} -.icon-signal-full:before { - content: "\e100"; -} -.icon-signal-high:before { - content: "\e101"; -} -.icon-signal-no:before { - content: "\e102"; -} -.icon-signal-poor:before { - content: "\e103"; -} -.icon-signal-weak:before { - content: "\e104"; -} -.icon-smartphone:before { - content: "\e105"; -} -.icon-tape:before { - content: "\e106"; -} -.icon-camera-symbol-1:before { - content: "\e107"; -} -.icon-camera-symbol-2:before { - content: "\e108"; -} -.icon-camera-symbol-3:before { - content: "\e109"; -} -.icon-headphone:before { - content: "\e10a"; -} -.icon-antenna-1:before { - content: "\e10b"; -} -.icon-antenna-2:before { - content: "\e10c"; -} -.icon-antenna-3:before { - content: "\e10d"; -} -.icon-hotspot-1:before { - content: "\e10e"; -} -.icon-hotspot-2:before { - content: "\e10f"; -} -.icon-link:before { - content: "\e110"; -} -.icon-megaphone-1:before { - content: "\e111"; -} -.icon-megaphone-2:before { - content: "\e112"; -} -.icon-radar:before { - content: "\e113"; -} -.icon-rss-1:before { - content: "\e114"; -} -.icon-rss-2:before { - content: "\e115"; -} -.icon-satellite:before { - content: "\e116"; -} -.icon-address-1:before { - content: "\e117"; -} -.icon-address-2:before { - content: "\e118"; -} -.icon-address-3:before { - content: "\e119"; -} -.icon-forward:before { - content: "\e11a"; -} -.icon-inbox-1:before { - content: "\e11b"; -} -.icon-inbox-2:before { - content: "\e11c"; -} -.icon-inbox-3:before { - content: "\e11d"; -} -.icon-inbox-4:before { - content: "\e11e"; -} -.icon-letter-1:before { - content: "\e11f"; -} -.icon-letter-2:before { - content: "\e120"; -} -.icon-letter-3:before { - content: "\e121"; -} -.icon-letter-4:before { - content: "\e122"; -} -.icon-letter-5:before { - content: "\e123"; -} -.icon-mail-1:before { - content: "\e124"; -} -.icon-mail-2:before { - content: "\e125"; -} -.icon-mail-add:before { - content: "\e126"; -} -.icon-mail-attention:before { - content: "\e127"; -} -.icon-mail-block:before { - content: "\e128"; -} -.icon-mail-box-1:before { - content: "\e129"; -} -.icon-mail-box-2:before { - content: "\e12a"; -} -.icon-mail-box-3:before { - content: "\e12b"; -} -.icon-mail-checked:before { - content: "\e12c"; -} -.icon-mail-compose:before { - content: "\e12d"; -} -.icon-mail-delete:before { - content: "\e12e"; -} -.icon-mail-favorite:before { - content: "\e12f"; -} -.icon-mail-inbox:before { - content: "\e130"; -} -.icon-mail-lock:before { - content: "\e131"; -} -.icon-mail-minus:before { - content: "\e132"; -} -.icon-mail-read:before { - content: "\e133"; -} -.icon-mail-recieved-1:before { - content: "\e134"; -} -.icon-mail-recieved-2:before { - content: "\e135"; -} -.icon-mail-search-1:before { - content: "\e136"; -} -.icon-mail-search-2:before { - content: "\e137"; -} -.icon-mail-sent-1:before { - content: "\e138"; -} -.icon-mail-sent-2:before { - content: "\e139"; -} -.icon-mail-setting:before { - content: "\e13a"; -} -.icon-mail-star:before { - content: "\e13b"; -} -.icon-mail-sync:before { - content: "\e13c"; -} -.icon-mail-time:before { - content: "\e13d"; -} -.icon-outbox-1:before { - content: "\e13e"; -} -.icon-outbox-2:before { - content: "\e13f"; -} -.icon-plane-paper-1:before { - content: "\e140"; -} -.icon-plane-paper-2:before { - content: "\e141"; -} -.icon-reply-mail-1:before { - content: "\e142"; -} -.icon-reply-mail-2:before { - content: "\e143"; -} -.icon-connection-1:before { - content: "\e144"; -} -.icon-connection-2:before { - content: "\e145"; -} -.icon-connection-3:before { - content: "\e146"; -} -.icon-contacts-1:before { - content: "\e147"; -} -.icon-contacts-2:before { - content: "\e148"; -} -.icon-contacts-3:before { - content: "\e149"; -} -.icon-contacts-4:before { - content: "\e14a"; -} -.icon-female:before { - content: "\e14b"; -} -.icon-gender:before { - content: "\e14c"; -} -.icon-gender-female:before { - content: "\e14d"; -} -.icon-gender-male:before { - content: "\e14e"; -} -.icon-id-1:before { - content: "\e14f"; -} -.icon-id-2:before { - content: "\e150"; -} -.icon-id-3:before { - content: "\e151"; -} -.icon-id-4:before { - content: "\e152"; -} -.icon-id-5:before { - content: "\e153"; -} -.icon-id-6:before { - content: "\e154"; -} -.icon-id-7:before { - content: "\e155"; -} -.icon-id-8:before { - content: "\e156"; -} -.icon-male:before { - content: "\e157"; -} -.icon-profile-1:before { - content: "\e158"; -} -.icon-profile-2:before { - content: "\e159"; -} -.icon-profile-3:before { - content: "\e15a"; -} -.icon-profile-4:before { - content: "\e15b"; -} -.icon-profile-5:before { - content: "\e15c"; -} -.icon-profile-6:before { - content: "\e15d"; -} -.icon-profile-athlete:before { - content: "\e15e"; -} -.icon-profile-bussiness-man:before { - content: "\e15f"; -} -.icon-profile-cook:before { - content: "\e160"; -} -.icon-profile-cop:before { - content: "\e161"; -} -.icon-profile-doctor-1:before { - content: "\e162"; -} -.icon-profile-doctor-2:before { - content: "\e163"; -} -.icon-profile-gentleman-1:before { - content: "\e164"; -} -.icon-profile-gentleman-2:before { - content: "\e165"; -} -.icon-profile-graduate:before { - content: "\e166"; -} -.icon-profile-king:before { - content: "\e167"; -} -.icon-profile-lady-1:before { - content: "\e168"; -} -.icon-profile-lady-2:before { - content: "\e169"; -} -.icon-profile-man:before { - content: "\e16a"; -} -.icon-profile-nurse-1:before { - content: "\e16b"; -} -.icon-profile-nurse-2:before { - content: "\e16c"; -} -.icon-profile-prisoner:before { - content: "\e16d"; -} -.icon-profile-serviceman-1:before { - content: "\e16e"; -} -.icon-profile-serviceman-2:before { - content: "\e16f"; -} -.icon-profile-spy:before { - content: "\e170"; -} -.icon-profile-teacher:before { - content: "\e171"; -} -.icon-profile-thief:before { - content: "\e172"; -} -.icon-user-1:before { - content: "\e173"; -} -.icon-user-2:before { - content: "\e174"; -} -.icon-user-add-1:before { - content: "\e175"; -} -.icon-user-add-2:before { - content: "\e176"; -} -.icon-user-block-1:before { - content: "\e177"; -} -.icon-user-block-2:before { - content: "\e178"; -} -.icon-user-checked-1:before { - content: "\e179"; -} -.icon-user-checked-2:before { - content: "\e17a"; -} -.icon-user-delete-1:before { - content: "\e17b"; -} -.icon-user-delete-2:before { - content: "\e17c"; -} -.icon-user-edit-1:before { - content: "\e17d"; -} -.icon-user-edit-2:before { - content: "\e17e"; -} -.icon-user-heart-1:before { - content: "\e17f"; -} -.icon-user-heart-2:before { - content: "\e180"; -} -.icon-user-lock:before { - content: "\e181"; -} -.icon-user-lock-1:before { - content: "\e182"; -} -.icon-user-minus-1:before { - content: "\e183"; -} -.icon-user-minus-2:before { - content: "\e184"; -} -.icon-user-search-1:before { - content: "\e185"; -} -.icon-user-search-2:before { - content: "\e186"; -} -.icon-user-setting-1:before { - content: "\e187"; -} -.icon-user-setting-2:before { - content: "\e188"; -} -.icon-user-star-1:before { - content: "\e189"; -} -.icon-user-star-2:before { - content: "\e18a"; -} -.icon-basket-1:before { - content: "\e18b"; -} -.icon-basket-2:before { - content: "\e18c"; -} -.icon-basket-3:before { - content: "\e18d"; -} -.icon-basket-add:before { - content: "\e18e"; -} -.icon-basket-minus:before { - content: "\e18f"; -} -.icon-briefcase-2:before { - content: "\e190"; -} -.icon-cart-1:before { - content: "\e191"; -} -.icon-cart-2:before { - content: "\e192"; -} -.icon-cart-3:before { - content: "\e193"; -} -.icon-cart-4:before { - content: "\e194"; -} -.icon-cut:before { - content: "\e195"; -} -.icon-hand-bag-1:before { - content: "\e196"; -} -.icon-hand-bag-2:before { - content: "\e197"; -} -.icon-purse-1:before { - content: "\e198"; -} -.icon-purse-2:before { - content: "\e199"; -} -.icon-qr-code:before { - content: "\e19a"; -} -.icon-receipt-1:before { - content: "\e19b"; -} -.icon-receipt-2:before { - content: "\e19c"; -} -.icon-receipt-3:before { - content: "\e19d"; -} -.icon-receipt-4:before { - content: "\e19e"; -} -.icon-shopping-1:before { - content: "\e19f"; -} -.icon-shopping-bag-1:before { - content: "\e1a0"; -} -.icon-shopping-bag-2:before { - content: "\e1a1"; -} -.icon-shopping-bag-3:before { - content: "\e1a2"; -} -.icon-sign-new-1:before { - content: "\e1a3"; -} -.icon-sign-new-2:before { - content: "\e1a4"; -} -.icon-sign-park:before { - content: "\e1a5"; -} -.icon-sign-star:before { - content: "\e1a6"; -} -.icon-trolley-1:before { - content: "\e1a7"; -} -.icon-trolley-2:before { - content: "\e1a8"; -} -.icon-trolley-3:before { - content: "\e1a9"; -} -.icon-trolley-load:before { - content: "\e1aa"; -} -.icon-trolley-off:before { - content: "\e1ab"; -} -.icon-wallet-1:before { - content: "\e1ac"; -} -.icon-wallet-2:before { - content: "\e1ad"; -} -.icon-wallet-3:before { - content: "\e1ae"; -} -.icon-camera-1:before { - content: "\e1af"; -} -.icon-camera-2:before { - content: "\e1b0"; -} -.icon-camera-3:before { - content: "\e1b1"; -} -.icon-camera-4:before { - content: "\e1b2"; -} -.icon-camera-5:before { - content: "\e1b3"; -} -.icon-camera-back:before { - content: "\e1b4"; -} -.icon-camera-focus:before { - content: "\e1b5"; -} -.icon-camera-frames:before { - content: "\e1b6"; -} -.icon-camera-front:before { - content: "\e1b7"; -} -.icon-camera-graph-1:before { - content: "\e1b8"; -} -.icon-camera-graph-2:before { - content: "\e1b9"; -} -.icon-camera-landscape:before { - content: "\e1ba"; -} -.icon-camera-lens-1:before { - content: "\e1bb"; -} -.icon-camera-lens-2:before { - content: "\e1bc"; -} -.icon-camera-light:before { - content: "\e1bd"; -} -.icon-camera-portrait:before { - content: "\e1be"; -} -.icon-camera-view:before { - content: "\e1bf"; -} -.icon-film-1:before { - content: "\e1c0"; -} -.icon-film-2:before { - content: "\e1c1"; -} -.icon-photo-1:before { - content: "\e1c2"; -} -.icon-photo-2:before { - content: "\e1c3"; -} -.icon-photo-frame:before { - content: "\e1c4"; -} -.icon-photos-1:before { - content: "\e1c5"; -} -.icon-photos-2:before { - content: "\e1c6"; -} -.icon-polaroid:before { - content: "\e1c7"; -} -.icon-signal-camera-1:before { - content: "\e1c8"; -} -.icon-signal-camera-2:before { - content: "\e1c9"; -} -.icon-user-photo:before { - content: "\e1ca"; -} -.icon-backward-1:before { - content: "\e1cb"; -} -.icon-dvd-player:before { - content: "\e1cc"; -} -.icon-eject-1:before { - content: "\e1cd"; -} -.icon-film-3:before { - content: "\e1ce"; -} -.icon-forward-1:before { - content: "\e1cf"; -} -.icon-handy-cam:before { - content: "\e1d0"; -} -.icon-movie-play-1:before { - content: "\e1d1"; -} -.icon-movie-play-2:before { - content: "\e1d2"; -} -.icon-movie-play-3:before { - content: "\e1d3"; -} -.icon-next-1:before { - content: "\e1d4"; -} -.icon-pause-1:before { - content: "\e1d5"; -} -.icon-play-1:before { - content: "\e1d6"; -} -.icon-player:before { - content: "\e1d7"; -} -.icon-previous-1:before { - content: "\e1d8"; -} -.icon-record-1:before { - content: "\e1d9"; -} -.icon-slate:before { - content: "\e1da"; -} -.icon-stop-1:before { - content: "\e1db"; -} -.icon-television:before { - content: "\e1dc"; -} -.icon-video-camera-1:before { - content: "\e1dd"; -} -.icon-video-camera-2:before { - content: "\e1de"; -} -.icon-backward-2:before { - content: "\e1df"; -} -.icon-cd:before { - content: "\e1e0"; -} -.icon-eject-2:before { - content: "\e1e1"; -} -.icon-equalizer-1:before { - content: "\e1e2"; -} -.icon-equalizer-2:before { - content: "\e1e3"; -} -.icon-forward-2:before { - content: "\e1e4"; -} -.icon-gramophone:before { - content: "\e1e5"; -} -.icon-gramophone-record:before { - content: "\e1e6"; -} -.icon-guitar:before { - content: "\e1e7"; -} -.icon-headphone-1:before { - content: "\e1e8"; -} -.icon-headphone-2:before { - content: "\e1e9"; -} -.icon-microphone-1:before { - content: "\e1ea"; -} -.icon-microphone-2:before { - content: "\e1eb"; -} -.icon-microphone-3:before { - content: "\e1ec"; -} -.icon-movie-play-4:before { - content: "\e1ed"; -} -.icon-music-note-1:before { - content: "\e1ee"; -} -.icon-music-note-3:before { - content: "\e1ef"; -} -.icon-music-note-4:before { - content: "\e1f0"; -} -.icon-music-note-5:before { - content: "\e1f1"; -} -.icon-next-2:before { - content: "\e1f2"; -} -.icon-notes-1:before { - content: "\e1f3"; -} -.icon-notes-2:before { - content: "\e1f4"; -} -.icon-pause-2:before { - content: "\e1f5"; -} -.icon-piano:before { - content: "\e1f6"; -} -.icon-play-2:before { - content: "\e1f7"; -} -.icon-playlist:before { - content: "\e1f8"; -} -.icon-previous-2:before { - content: "\e1f9"; -} -.icon-radio-1:before { - content: "\e1fa"; -} -.icon-radio-2:before { - content: "\e1fb"; -} -.icon-record-2:before { - content: "\e1fc"; -} -.icon-recorder:before { - content: "\e1fd"; -} -.icon-saxophone:before { - content: "\e1fe"; -} -.icon-speaker-1:before { - content: "\e1ff"; -} -.icon-speaker-2:before { - content: "\e200"; -} -.icon-speaker-3:before { - content: "\e201"; -} -.icon-stop-2:before { - content: "\e202"; -} -.icon-tape-1:before { - content: "\e203"; -} -.icon-trumpet:before { - content: "\e204"; -} -.icon-volume-down-1:before { - content: "\e205"; -} -.icon-volume-down-2:before { - content: "\e206"; -} -.icon-volume-loud-1:before { - content: "\e207"; -} -.icon-volume-loud-2:before { - content: "\e208"; -} -.icon-volume-low-1:before { - content: "\e209"; -} -.icon-volume-low-2:before { - content: "\e20a"; -} -.icon-volume-medium-1:before { - content: "\e20b"; -} -.icon-volume-medium-2:before { - content: "\e20c"; -} -.icon-volume-mute-1:before { - content: "\e20d"; -} -.icon-volume-mute-2:before { - content: "\e20e"; -} -.icon-volume-mute-3:before { - content: "\e20f"; -} -.icon-volume-up-1:before { - content: "\e210"; -} -.icon-volume-up-2:before { - content: "\e211"; -} -.icon-walkman:before { - content: "\e212"; -} -.icon-cloud:before { - content: "\e213"; -} -.icon-cloud-add:before { - content: "\e214"; -} -.icon-cloud-checked:before { - content: "\e215"; -} -.icon-cloud-delete:before { - content: "\e216"; -} -.icon-cloud-download:before { - content: "\e217"; -} -.icon-cloud-minus:before { - content: "\e218"; -} -.icon-cloud-refresh:before { - content: "\e219"; -} -.icon-cloud-sync:before { - content: "\e21a"; -} -.icon-cloud-upload:before { - content: "\e21b"; -} -.icon-download-1:before { - content: "\e21c"; -} -.icon-download-2:before { - content: "\e21d"; -} -.icon-download-3:before { - content: "\e21e"; -} -.icon-download-4:before { - content: "\e21f"; -} -.icon-download-5:before { - content: "\e220"; -} -.icon-download-6:before { - content: "\e221"; -} -.icon-download-7:before { - content: "\e222"; -} -.icon-download-8:before { - content: "\e223"; -} -.icon-download-9:before { - content: "\e224"; -} -.icon-download-10:before { - content: "\e225"; -} -.icon-download-11:before { - content: "\e226"; -} -.icon-download-12:before { - content: "\e227"; -} -.icon-download-13:before { - content: "\e228"; -} -.icon-download-14:before { - content: "\e229"; -} -.icon-download-15:before { - content: "\e22a"; -} -.icon-download-file:before { - content: "\e22b"; -} -.icon-download-folder:before { - content: "\e22c"; -} -.icon-goal-1:before { - content: "\e22d"; -} -.icon-goal-2:before { - content: "\e22e"; -} -.icon-transfer-1:before { - content: "\e22f"; -} -.icon-transfer-2:before { - content: "\e230"; -} -.icon-transfer-3:before { - content: "\e231"; -} -.icon-transfer-4:before { - content: "\e232"; -} -.icon-transfer-5:before { - content: "\e233"; -} -.icon-transfer-6:before { - content: "\e234"; -} -.icon-transfer-7:before { - content: "\e235"; -} -.icon-transfer-8:before { - content: "\e236"; -} -.icon-transfer-9:before { - content: "\e237"; -} -.icon-transfer-10:before { - content: "\e238"; -} -.icon-transfer-11:before { - content: "\e239"; -} -.icon-transfer-12:before { - content: "\e23a"; -} -.icon-upload-1:before { - content: "\e23b"; -} -.icon-upload-2:before { - content: "\e23c"; -} -.icon-upload-3:before { - content: "\e23d"; -} -.icon-upload-4:before { - content: "\e23e"; -} -.icon-upload-5:before { - content: "\e23f"; -} -.icon-upload-6:before { - content: "\e240"; -} -.icon-upload-7:before { - content: "\e241"; -} -.icon-upload-8:before { - content: "\e242"; -} -.icon-upload-9:before { - content: "\e243"; -} -.icon-upload-10:before { - content: "\e244"; -} -.icon-upload-11:before { - content: "\e245"; -} -.icon-upload-12:before { - content: "\e246"; -} -.icon-clipboard-1:before { - content: "\e247"; -} -.icon-clipboard-2:before { - content: "\e248"; -} -.icon-clipboard-3:before { - content: "\e249"; -} -.icon-clipboard-add:before { - content: "\e24a"; -} -.icon-clipboard-block:before { - content: "\e24b"; -} -.icon-clipboard-checked:before { - content: "\e24c"; -} -.icon-clipboard-delete:before { - content: "\e24d"; -} -.icon-clipboard-edit:before { - content: "\e24e"; -} -.icon-clipboard-minus:before { - content: "\e24f"; -} -.icon-document-1:before { - content: "\e250"; -} -.icon-document-2:before { - content: "\e251"; -} -.icon-file-1:before { - content: "\e252"; -} -.icon-file-2:before { - content: "\e253"; -} -.icon-file-add:before { - content: "\e254"; -} -.icon-file-attention:before { - content: "\e255"; -} -.icon-file-block:before { - content: "\e256"; -} -.icon-file-bookmark:before { - content: "\e257"; -} -.icon-file-checked:before { - content: "\e258"; -} -.icon-file-code:before { - content: "\e259"; -} -.icon-file-delete:before { - content: "\e25a"; -} -.icon-file-download:before { - content: "\e25b"; -} -.icon-file-edit:before { - content: "\e25c"; -} -.icon-file-favorite-1:before { - content: "\e25d"; -} -.icon-file-favorite-2:before { - content: "\e25e"; -} -.icon-file-graph-1:before { - content: "\e25f"; -} -.icon-file-graph-2:before { - content: "\e260"; -} -.icon-file-home:before { - content: "\e261"; -} -.icon-file-image-1:before { - content: "\e262"; -} -.icon-file-image-2:before { - content: "\e263"; -} -.icon-file-list:before { - content: "\e264"; -} -.icon-file-lock:before { - content: "\e265"; -} -.icon-file-media:before { - content: "\e266"; -} -.icon-file-minus:before { - content: "\e267"; -} -.icon-file-music:before { - content: "\e268"; -} -.icon-file-new:before { - content: "\e269"; -} -.icon-file-registry:before { - content: "\e26a"; -} -.icon-file-search:before { - content: "\e26b"; -} -.icon-file-setting:before { - content: "\e26c"; -} -.icon-file-sync:before { - content: "\e26d"; -} -.icon-file-table:before { - content: "\e26e"; -} -.icon-file-thumbnail:before { - content: "\e26f"; -} -.icon-file-time:before { - content: "\e270"; -} -.icon-file-transfer:before { - content: "\e271"; -} -.icon-file-upload:before { - content: "\e272"; -} -.icon-file-zip:before { - content: "\e273"; -} -.icon-files-1:before { - content: "\e274"; -} -.icon-files-2:before { - content: "\e275"; -} -.icon-files-3:before { - content: "\e276"; -} -.icon-files-4:before { - content: "\e277"; -} -.icon-files-5:before { - content: "\e278"; -} -.icon-files-6:before { - content: "\e279"; -} -.icon-hand-file-1:before { - content: "\e27a"; -} -.icon-hand-file-2:before { - content: "\e27b"; -} -.icon-note-paper-1:before { - content: "\e27c"; -} -.icon-note-paper-2:before { - content: "\e27d"; -} -.icon-note-paper-add:before { - content: "\e27e"; -} -.icon-note-paper-attention:before { - content: "\e27f"; -} -.icon-note-paper-block:before { - content: "\e280"; -} -.icon-note-paper-checked:before { - content: "\e281"; -} -.icon-note-paper-delete:before { - content: "\e282"; -} -.icon-note-paper-download:before { - content: "\e283"; -} -.icon-note-paper-edit:before { - content: "\e284"; -} -.icon-note-paper-favorite:before { - content: "\e285"; -} -.icon-note-paper-lock:before { - content: "\e286"; -} -.icon-note-paper-minus:before { - content: "\e287"; -} -.icon-note-paper-search:before { - content: "\e288"; -} -.icon-note-paper-sync:before { - content: "\e289"; -} -.icon-note-paper-upload:before { - content: "\e28a"; -} -.icon-print:before { - content: "\e28b"; -} -.icon-folder-1:before { - content: "\e28c"; -} -.icon-folder-2:before { - content: "\e28d"; -} -.icon-folder-3:before { - content: "\e28e"; -} -.icon-folder-4:before { - content: "\e28f"; -} -.icon-folder-add:before { - content: "\e290"; -} -.icon-folder-attention:before { - content: "\e291"; -} -.icon-folder-block:before { - content: "\e292"; -} -.icon-folder-bookmark:before { - content: "\e293"; -} -.icon-folder-checked:before { - content: "\e294"; -} -.icon-folder-code:before { - content: "\e295"; -} -.icon-folder-delete:before { - content: "\e296"; -} -.icon-folder-download:before { - content: "\e297"; -} -.icon-folder-edit:before { - content: "\e298"; -} -.icon-folder-favorite:before { - content: "\e299"; -} -.icon-folder-home:before { - content: "\e29a"; -} -.icon-folder-image:before { - content: "\e29b"; -} -.icon-folder-lock:before { - content: "\e29c"; -} -.icon-folder-media:before { - content: "\e29d"; -} -.icon-folder-minus:before { - content: "\e29e"; -} -.icon-folder-music:before { - content: "\e29f"; -} -.icon-folder-new:before { - content: "\e2a0"; -} -.icon-folder-search:before { - content: "\e2a1"; -} -.icon-folder-setting:before { - content: "\e2a2"; -} -.icon-folder-share-1:before { - content: "\e2a3"; -} -.icon-folder-share-2:before { - content: "\e2a4"; -} -.icon-folder-sync:before { - content: "\e2a5"; -} -.icon-folder-transfer:before { - content: "\e2a6"; -} -.icon-folder-upload:before { - content: "\e2a7"; -} -.icon-folder-zip:before { - content: "\e2a8"; -} -.icon-add-1:before { - content: "\e2a9"; -} -.icon-add-2:before { - content: "\e2aa"; -} -.icon-add-3:before { - content: "\e2ab"; -} -.icon-add-4:before { - content: "\e2ac"; -} -.icon-add-tag:before { - content: "\e2ad"; -} -.icon-arrow-1:before { - content: "\e2ae"; -} -.icon-arrow-2:before { - content: "\e2af"; -} -.icon-arrow-down-1:before { - content: "\e2b0"; -} -.icon-arrow-down-2:before { - content: "\e2b1"; -} -.icon-arrow-left-1:before { - content: "\e2b2"; -} -.icon-arrow-left-2:before { - content: "\e2b3"; -} -.icon-arrow-move-1:before { - content: "\e2b4"; -} -.icon-arrow-move-down:before { - content: "\e2b5"; -} -.icon-arrow-move-left:before { - content: "\e2b6"; -} -.icon-arrow-move-right:before { - content: "\e2b7"; -} -.icon-arrow-move-up:before { - content: "\e2b8"; -} -.icon-arrow-right-1:before { - content: "\e2b9"; -} -.icon-arrow-right-2:before { - content: "\e2ba"; -} -.icon-arrow-up-1:before { - content: "\e2bb"; -} -.icon-arrow-up-2:before { - content: "\e2bc"; -} -.icon-back:before { - content: "\e2bd"; -} -.icon-center-expand:before { - content: "\e2be"; -} -.icon-center-reduce:before { - content: "\e2bf"; -} -.icon-delete-1-1:before { - content: "\e2c0"; -} -.icon-delete-2-1:before { - content: "\e2c1"; -} -.icon-delete-3:before { - content: "\e2c2"; -} -.icon-delete-4:before { - content: "\e2c3"; -} -.icon-delete-tag:before { - content: "\e2c4"; -} -.icon-expand-horizontal:before { - content: "\e2c5"; -} -.icon-expand-vertical:before { - content: "\e2c6"; -} -.icon-forward-3:before { - content: "\e2c7"; -} -.icon-infinity:before { - content: "\e2c8"; -} -.icon-loading:before { - content: "\e2c9"; -} -.icon-log-out-1:before { - content: "\e2ca"; -} -.icon-loop-1:before { - content: "\e2cb"; -} -.icon-loop-2:before { - content: "\e2cc"; -} -.icon-loop-3:before { - content: "\e2cd"; -} -.icon-minus-1:before { - content: "\e2ce"; -} -.icon-minus-2:before { - content: "\e2cf"; -} -.icon-minus-3:before { - content: "\e2d0"; -} -.icon-minus-4:before { - content: "\e2d1"; -} -.icon-minus-tag:before { - content: "\e2d2"; -} -.icon-move-diagonal-1:before { - content: "\e2d3"; -} -.icon-move-diagonal-2:before { - content: "\e2d4"; -} -.icon-move-horizontal-1:before { - content: "\e2d5"; -} -.icon-move-horizontal-2:before { - content: "\e2d6"; -} -.icon-move-vertical-1:before { - content: "\e2d7"; -} -.icon-move-vertical-2:before { - content: "\e2d8"; -} -.icon-next-1-1:before { - content: "\e2d9"; -} -.icon-next-2-1:before { - content: "\e2da"; -} -.icon-power-1-1:before { - content: "\e2db"; -} -.icon-power-2-1:before { - content: "\e2dc"; -} -.icon-power-3:before { - content: "\e2dd"; -} -.icon-power-4:before { - content: "\e2de"; -} -.icon-power-5:before { - content: "\e2df"; -} -.icon-recycle:before { - content: "\e2e0"; -} -.icon-refresh:before { - content: "\e2e1"; -} -.icon-repeat:before { - content: "\e2e2"; -} -.icon-return:before { - content: "\e2e3"; -} -.icon-scale-all-1:before { - content: "\e2e4"; -} -.icon-scale-center:before { - content: "\e2e5"; -} -.icon-scale-horizontal-1:before { - content: "\e2e6"; -} -.icon-scale-horizontal-2:before { - content: "\e2e7"; -} -.icon-scale-reduce-1:before { - content: "\e2e8"; -} -.icon-scale-reduce-2:before { - content: "\e2e9"; -} -.icon-scale-reduce-3:before { - content: "\e2ea"; -} -.icon-scale-spread-1:before { - content: "\e2eb"; -} -.icon-scale-spread-2:before { - content: "\e2ec"; -} -.icon-scale-spread-3:before { - content: "\e2ed"; -} -.icon-scale-vertical-1:before { - content: "\e2ee"; -} -.icon-scale-vertical-2:before { - content: "\e2ef"; -} -.icon-scroll-horizontal-1:before { - content: "\e2f0"; -} -.icon-scroll-horizontal-2:before { - content: "\e2f1"; -} -.icon-scroll-omnidirectional-1:before { - content: "\e2f2"; -} -.icon-scroll-omnidirectional-2:before { - content: "\e2f3"; -} -.icon-scroll-vertical-1:before { - content: "\e2f4"; -} -.icon-scroll-vertical-2:before { - content: "\e2f5"; -} -.icon-shuffle:before { - content: "\e2f6"; -} -.icon-split:before { - content: "\e2f7"; -} -.icon-sync-1:before { - content: "\e2f8"; -} -.icon-sync-2:before { - content: "\e2f9"; -} -.icon-timer:before { - content: "\e2fa"; -} -.icon-transfer:before { - content: "\e2fb"; -} -.icon-transfer-1-1:before { - content: "\e2fc"; -} -.icon-chat-1-1:before { - content: "\e2fd"; -} -.icon-chat-2-1:before { - content: "\e2fe"; -} -.icon-check-1:before { - content: "\e2ff"; -} -.icon-check-2:before { - content: "\e300"; -} -.icon-check-3:before { - content: "\e301"; -} -.icon-check-4:before { - content: "\e302"; -} -.icon-check-bubble:before { - content: "\e303"; -} -.icon-check-list:before { - content: "\e304"; -} -.icon-check-shield:before { - content: "\e305"; -} -.icon-cross-1:before { - content: "\e306"; -} -.icon-cross-bubble:before { - content: "\e307"; -} -.icon-cross-shield:before { - content: "\e308"; -} -.icon-briefcase:before { - content: "\e309"; -} -.icon-brightness-high:before { - content: "\e30a"; -} -.icon-brightness-low:before { - content: "\e30b"; -} -.icon-hammer-1:before { - content: "\e30c"; -} -.icon-hammer-2:before { - content: "\e30d"; -} -.icon-pulse:before { - content: "\e30e"; -} -.icon-scale:before { - content: "\e30f"; -} -.icon-screw-driver:before { - content: "\e310"; -} -.icon-setting-adjustment:before { - content: "\e311"; -} -.icon-setting-gear:before { - content: "\e312"; -} -.icon-setting-gears-1:before { - content: "\e313"; -} -.icon-setting-gears-2:before { - content: "\e314"; -} -.icon-setting-wrenches:before { - content: "\e315"; -} -.icon-switch-1:before { - content: "\e316"; -} -.icon-switch-2:before { - content: "\e317"; -} -.icon-wrench:before { - content: "\e318"; -} -.icon-alarm-1:before { - content: "\e319"; -} -.icon-alarm-clock:before { - content: "\e31a"; -} -.icon-alarm-no:before { - content: "\e31b"; -} -.icon-alarm-snooze:before { - content: "\e31c"; -} -.icon-bell:before { - content: "\e31d"; -} -.icon-calendar-1:before { - content: "\e31e"; -} -.icon-calendar-2:before { - content: "\e31f"; -} -.icon-clock-1:before { - content: "\e320"; -} -.icon-clock-2:before { - content: "\e321"; -} -.icon-clock-3:before { - content: "\e322"; -} -.icon-hourglass-1:before { - content: "\e323"; -} -.icon-hourglass-2:before { - content: "\e324"; -} -.icon-timer-1:before { - content: "\e325"; -} -.icon-timer-3-quarter-1:before { - content: "\e326"; -} -.icon-timer-3-quarter-2:before { - content: "\e327"; -} -.icon-timer-full-1:before { - content: "\e328"; -} -.icon-timer-full-2:before { - content: "\e329"; -} -.icon-timer-half-1:before { - content: "\e32a"; -} -.icon-timer-half-2:before { - content: "\e32b"; -} -.icon-timer-half-3:before { - content: "\e32c"; -} -.icon-timer-half-4:before { - content: "\e32d"; -} -.icon-timer-quarter-1:before { - content: "\e32e"; -} -.icon-timer-quarter-2:before { - content: "\e32f"; -} -.icon-watch-1:before { - content: "\e330"; -} -.icon-watch-2:before { - content: "\e331"; -} -.icon-alert-1:before { - content: "\e332"; -} -.icon-alert-2:before { - content: "\e333"; -} -.icon-alert-3:before { - content: "\e334"; -} -.icon-information:before { - content: "\e335"; -} -.icon-nuclear-1:before { - content: "\e336"; -} -.icon-nuclear-2:before { - content: "\e337"; -} -.icon-question-mark:before { - content: "\e338"; -} -.icon-abacus:before { - content: "\e339"; -} -.icon-amex-card:before { - content: "\e33a"; -} -.icon-atm:before { - content: "\e33b"; -} -.icon-balance:before { - content: "\e33c"; -} -.icon-bank-1:before { - content: "\e33d"; -} -.icon-bank-2:before { - content: "\e33e"; -} -.icon-bank-note:before { - content: "\e33f"; -} -.icon-bank-notes-1:before { - content: "\e340"; -} -.icon-bank-notes-2:before { - content: "\e341"; -} -.icon-bitcoins:before { - content: "\e342"; -} -.icon-board-1:before { - content: "\e343"; -} -.icon-box-1:before { - content: "\e344"; -} -.icon-box-2:before { - content: "\e345"; -} -.icon-box-3:before { - content: "\e346"; -} -.icon-box-download:before { - content: "\e347"; -} -.icon-box-shipping:before { - content: "\e348"; -} -.icon-box-upload:before { - content: "\e349"; -} -.icon-business-chart-1:before { - content: "\e34a"; -} -.icon-business-chart-2:before { - content: "\e34b"; -} -.icon-calculator-1:before { - content: "\e34c"; -} -.icon-calculator-2:before { - content: "\e34d"; -} -.icon-calculator-3:before { - content: "\e34e"; -} -.icon-cash-register:before { - content: "\e34f"; -} -.icon-chart-board:before { - content: "\e350"; -} -.icon-chart-down:before { - content: "\e351"; -} -.icon-chart-up:before { - content: "\e352"; -} -.icon-check:before { - content: "\e353"; -} -.icon-coins-1:before { - content: "\e354"; -} -.icon-coins-2:before { - content: "\e355"; -} -.icon-court:before { - content: "\e356"; -} -.icon-credit-card:before { - content: "\e357"; -} -.icon-credit-card-lock:before { - content: "\e358"; -} -.icon-delivery:before { - content: "\e359"; -} -.icon-dollar-bag:before { - content: "\e35a"; -} -.icon-dollar-currency-1:before { - content: "\e35b"; -} -.icon-dollar-currency-2:before { - content: "\e35c"; -} -.icon-dollar-currency-3:before { - content: "\e35d"; -} -.icon-dollar-currency-4:before { - content: "\e35e"; -} -.icon-euro-bag:before { - content: "\e35f"; -} -.icon-euro-currency-1:before { - content: "\e360"; -} -.icon-euro-currency-2:before { - content: "\e361"; -} -.icon-euro-currency-3:before { - content: "\e362"; -} -.icon-euro-currency-4:before { - content: "\e363"; -} -.icon-forklift:before { - content: "\e364"; -} -.icon-hand-card:before { - content: "\e365"; -} -.icon-hand-coin:before { - content: "\e366"; -} -.icon-keynote:before { - content: "\e367"; -} -.icon-master-card:before { - content: "\e368"; -} -.icon-money:before { - content: "\e369"; -} -.icon-parking-meter:before { - content: "\e36a"; -} -.icon-percent-1:before { - content: "\e36b"; -} -.icon-percent-2:before { - content: "\e36c"; -} -.icon-percent-3:before { - content: "\e36d"; -} -.icon-percent-4:before { - content: "\e36e"; -} -.icon-percent-5:before { - content: "\e36f"; -} -.icon-percent-up:before { - content: "\e370"; -} -.icon-pie-chart-1:before { - content: "\e371"; -} -.icon-pie-chart-2:before { - content: "\e372"; -} -.icon-piggy-bank:before { - content: "\e373"; -} -.icon-pound-currency-1:before { - content: "\e374"; -} -.icon-pound-currency-2:before { - content: "\e375"; -} -.icon-pound-currency-3:before { - content: "\e376"; -} -.icon-pound-currency-4:before { - content: "\e377"; -} -.icon-safe-1:before { - content: "\e378"; -} -.icon-safe-2:before { - content: "\e379"; -} -.icon-shop:before { - content: "\e37a"; -} -.icon-sign:before { - content: "\e37b"; -} -.icon-trolley:before { - content: "\e37c"; -} -.icon-truck-1:before { - content: "\e37d"; -} -.icon-truck-2:before { - content: "\e37e"; -} -.icon-visa-card:before { - content: "\e37f"; -} -.icon-yen-currency-1:before { - content: "\e380"; -} -.icon-yen-currency-2:before { - content: "\e381"; -} -.icon-yen-currency-3:before { - content: "\e382"; -} -.icon-yen-currency-4:before { - content: "\e383"; -} -.icon-add-marker-1:before { - content: "\e384"; -} -.icon-add-marker-2:before { - content: "\e385"; -} -.icon-add-marker-3:before { - content: "\e386"; -} -.icon-add-marker-4:before { - content: "\e387"; -} -.icon-add-marker-5:before { - content: "\e388"; -} -.icon-compass-1:before { - content: "\e389"; -} -.icon-compass-2:before { - content: "\e38a"; -} -.icon-compass-3:before { - content: "\e38b"; -} -.icon-delete-marker-1:before { - content: "\e38c"; -} -.icon-delete-marker-2:before { - content: "\e38d"; -} -.icon-delete-marker-3:before { - content: "\e38e"; -} -.icon-delete-marker-4:before { - content: "\e38f"; -} -.icon-delete-marker-5:before { - content: "\e390"; -} -.icon-favorite-marker:before { - content: "\e391"; -} -.icon-favorite-marker-1:before { - content: "\e392"; -} -.icon-favorite-marker-2:before { - content: "\e393"; -} -.icon-favorite-marker-3:before { - content: "\e394"; -} -.icon-globe:before { - content: "\e395"; -} -.icon-location:before { - content: "\e396"; -} -.icon-map-1:before { - content: "\e397"; -} -.icon-map-location:before { - content: "\e398"; -} -.icon-map-marker-1:before { - content: "\e399"; -} -.icon-map-marker-2:before { - content: "\e39a"; -} -.icon-map-marker-3:before { - content: "\e39b"; -} -.icon-map-marker-4:before { - content: "\e39c"; -} -.icon-map-pin:before { - content: "\e39d"; -} -.icon-map-pin-marker:before { - content: "\e39e"; -} -.icon-marker-1:before { - content: "\e39f"; -} -.icon-marker-2:before { - content: "\e3a0"; -} -.icon-marker-3:before { - content: "\e3a1"; -} -.icon-marker-4:before { - content: "\e3a2"; -} -.icon-minus-marker-1:before { - content: "\e3a3"; -} -.icon-minus-marker-2:before { - content: "\e3a4"; -} -.icon-minus-marker-3:before { - content: "\e3a5"; -} -.icon-minus-marker-4:before { - content: "\e3a6"; -} -.icon-pin-1-1:before { - content: "\e3a7"; -} -.icon-pin-2-1:before { - content: "\e3a8"; -} -.icon-pin-location:before { - content: "\e3a9"; -} -.icon-anchor:before { - content: "\e3aa"; -} -.icon-bank:before { - content: "\e3ab"; -} -.icon-beach:before { - content: "\e3ac"; -} -.icon-boat:before { - content: "\e3ad"; -} -.icon-building-1:before { - content: "\e3ae"; -} -.icon-building-2:before { - content: "\e3af"; -} -.icon-building-3:before { - content: "\e3b0"; -} -.icon-buildings-1:before { - content: "\e3b1"; -} -.icon-buildings-2:before { - content: "\e3b2"; -} -.icon-buildings-3:before { - content: "\e3b3"; -} -.icon-buildings-4:before { - content: "\e3b4"; -} -.icon-castle:before { - content: "\e3b5"; -} -.icon-column:before { - content: "\e3b6"; -} -.icon-direction-sign:before { - content: "\e3b7"; -} -.icon-factory:before { - content: "\e3b8"; -} -.icon-fence:before { - content: "\e3b9"; -} -.icon-garage:before { - content: "\e3ba"; -} -.icon-globe-1:before { - content: "\e3bb"; -} -.icon-globe-2:before { - content: "\e3bc"; -} -.icon-house-1:before { - content: "\e3bd"; -} -.icon-house-2:before { - content: "\e3be"; -} -.icon-house-3:before { - content: "\e3bf"; -} -.icon-house-4:before { - content: "\e3c0"; -} -.icon-library:before { - content: "\e3c1"; -} -.icon-light-house:before { - content: "\e3c2"; -} -.icon-pine-tree:before { - content: "\e3c3"; -} -.icon-pisa:before { - content: "\e3c4"; -} -.icon-skyscraper:before { - content: "\e3c5"; -} -.icon-temple:before { - content: "\e3c6"; -} -.icon-treasure-map:before { - content: "\e3c7"; -} -.icon-tree:before { - content: "\e3c8"; -} -.icon-attention:before { - content: "\e3c9"; -} -.icon-bug-1:before { - content: "\e3ca"; -} -.icon-bug-2:before { - content: "\e3cb"; -} -.icon-css3:before { - content: "\e3cc"; -} -.icon-firewall:before { - content: "\e3cd"; -} -.icon-html5:before { - content: "\e3ce"; -} -.icon-plugin-1:before { - content: "\e3cf"; -} -.icon-plugin-2:before { - content: "\e3d0"; -} -.icon-script:before { - content: "\e3d1"; -} -.icon-new-window:before { - content: "\e3d2"; -} -.icon-window-1:before { - content: "\e3d3"; -} -.icon-window-2:before { - content: "\e3d4"; -} -.icon-window-3:before { - content: "\e3d5"; -} -.icon-window-add:before { - content: "\e3d6"; -} -.icon-window-alert:before { - content: "\e3d7"; -} -.icon-window-check:before { - content: "\e3d8"; -} -.icon-window-code-1:before { - content: "\e3d9"; -} -.icon-window-code-2:before { - content: "\e3da"; -} -.icon-window-code-3:before { - content: "\e3db"; -} -.icon-window-column:before { - content: "\e3dc"; -} -.icon-window-delete:before { - content: "\e3dd"; -} -.icon-window-denied:before { - content: "\e3de"; -} -.icon-window-download-1:before { - content: "\e3df"; -} -.icon-window-download-2:before { - content: "\e3e0"; -} -.icon-window-edit:before { - content: "\e3e1"; -} -.icon-window-favorite-1:before { - content: "\e3e2"; -} -.icon-window-favorite-2:before { - content: "\e3e3"; -} -.icon-window-graph-1:before { - content: "\e3e4"; -} -.icon-window-graph-2:before { - content: "\e3e5"; -} -.icon-window-hand:before { - content: "\e3e6"; -} -.icon-window-home:before { - content: "\e3e7"; -} -.icon-window-list-1:before { - content: "\e3e8"; -} -.icon-window-list-2:before { - content: "\e3e9"; -} -.icon-window-lock:before { - content: "\e3ea"; -} -.icon-window-minimize:before { - content: "\e3eb"; -} -.icon-window-minus:before { - content: "\e3ec"; -} -.icon-window-refresh:before { - content: "\e3ed"; -} -.icon-window-registry:before { - content: "\e3ee"; -} -.icon-window-search:before { - content: "\e3ef"; -} -.icon-window-selection-1:before { - content: "\e3f0"; -} -.icon-window-selection-2:before { - content: "\e3f1"; -} -.icon-window-setting:before { - content: "\e3f2"; -} -.icon-window-sync:before { - content: "\e3f3"; -} -.icon-window-thumbnail-1:before { - content: "\e3f4"; -} -.icon-window-thumbnail-2:before { - content: "\e3f5"; -} -.icon-window-time:before { - content: "\e3f6"; -} -.icon-window-upload-1:before { - content: "\e3f7"; -} -.icon-window-upload-2:before { - content: "\e3f8"; -} -.icon-database:before { - content: "\e3f9"; -} -.icon-database-alert:before { - content: "\e3fa"; -} -.icon-database-block:before { - content: "\e3fb"; -} -.icon-database-check:before { - content: "\e3fc"; -} -.icon-database-delete:before { - content: "\e3fd"; -} -.icon-database-download:before { - content: "\e3fe"; -} -.icon-database-editor:before { - content: "\e3ff"; -} -.icon-database-lock:before { - content: "\e400"; -} -.icon-database-minus:before { - content: "\e401"; -} -.icon-database-network:before { - content: "\e402"; -} -.icon-database-plus:before { - content: "\e403"; -} -.icon-database-refresh:before { - content: "\e404"; -} -.icon-database-search:before { - content: "\e405"; -} -.icon-database-setting:before { - content: "\e406"; -} -.icon-database-sync:before { - content: "\e407"; -} -.icon-database-time:before { - content: "\e408"; -} -.icon-database-upload:before { - content: "\e409"; -} -.icon-battery-charging:before { - content: "\e40a"; -} -.icon-battery-full:before { - content: "\e40b"; -} -.icon-battery-high:before { - content: "\e40c"; -} -.icon-battery-low:before { - content: "\e40d"; -} -.icon-battery-medium:before { - content: "\e40e"; -} -.icon-cd-1:before { - content: "\e40f"; -} -.icon-cd-2:before { - content: "\e410"; -} -.icon-chip:before { - content: "\e411"; -} -.icon-computer:before { - content: "\e412"; -} -.icon-disc:before { - content: "\e413"; -} -.icon-filter:before { - content: "\e414"; -} -.icon-floppy-disk:before { - content: "\e415"; -} -.icon-gameboy:before { - content: "\e416"; -} -.icon-harddisk-1:before { - content: "\e417"; -} -.icon-harddisk-2:before { - content: "\e418"; -} -.icon-imac:before { - content: "\e419"; -} -.icon-ipad-1:before { - content: "\e41a"; -} -.icon-ipad-2:before { - content: "\e41b"; -} -.icon-ipod:before { - content: "\e41c"; -} -.icon-joystick-1:before { - content: "\e41d"; -} -.icon-joystick-2:before { - content: "\e41e"; -} -.icon-joystick-3:before { - content: "\e41f"; -} -.icon-keyboard-1:before { - content: "\e420"; -} -.icon-keyboard-2:before { - content: "\e421"; -} -.icon-kindle-1:before { - content: "\e422"; -} -.icon-kindle-2:before { - content: "\e423"; -} -.icon-laptop-1:before { - content: "\e424"; -} -.icon-laptop-2:before { - content: "\e425"; -} -.icon-memory-card:before { - content: "\e426"; -} -.icon-mobile-phone:before { - content: "\e427"; -} -.icon-mouse-1:before { - content: "\e428"; -} -.icon-mouse-2:before { - content: "\e429"; -} -.icon-mp3player:before { - content: "\e42a"; -} -.icon-plug-1:before { - content: "\e42b"; -} -.icon-plug-2:before { - content: "\e42c"; -} -.icon-plug-slot:before { - content: "\e42d"; -} -.icon-printer:before { - content: "\e42e"; -} -.icon-projector:before { - content: "\e42f"; -} -.icon-remote:before { - content: "\e430"; -} -.icon-router:before { - content: "\e431"; -} -.icon-screen-1:before { - content: "\e432"; -} -.icon-screen-2:before { - content: "\e433"; -} -.icon-screen-3:before { - content: "\e434"; -} -.icon-screen-4:before { - content: "\e435"; -} -.icon-smartphone-1:before { - content: "\e436"; -} -.icon-television-1:before { - content: "\e437"; -} -.icon-typewriter-1:before { - content: "\e438"; -} -.icon-typewriter-2:before { - content: "\e439"; -} -.icon-usb-1:before { - content: "\e43a"; -} -.icon-usb-2:before { - content: "\e43b"; -} -.icon-webcam:before { - content: "\e43c"; -} -.icon-wireless-router-1:before { - content: "\e43d"; -} -.icon-wireless-router-2:before { - content: "\e43e"; -} -.icon-bluetooth:before { - content: "\e43f"; -} -.icon-ethernet:before { - content: "\e440"; -} -.icon-ethernet-slot:before { - content: "\e441"; -} -.icon-firewire-1:before { - content: "\e442"; -} -.icon-firewire-2:before { - content: "\e443"; -} -.icon-network-1:before { - content: "\e444"; -} -.icon-network-2:before { - content: "\e445"; -} -.icon-server-1:before { - content: "\e446"; -} -.icon-server-2:before { - content: "\e447"; -} -.icon-server-3:before { - content: "\e448"; -} -.icon-usb:before { - content: "\e449"; -} -.icon-wireless-signal:before { - content: "\e44a"; -} -.icon-book:before { - content: "\e44b"; -} -.icon-book-1:before { - content: "\e44c"; -} -.icon-book-2:before { - content: "\e44d"; -} -.icon-book-3:before { - content: "\e44e"; -} -.icon-book-4:before { - content: "\e44f"; -} -.icon-book-5:before { - content: "\e450"; -} -.icon-book-6:before { - content: "\e451"; -} -.icon-book-7:before { - content: "\e452"; -} -.icon-book-download-1:before { - content: "\e453"; -} -.icon-book-download-2:before { - content: "\e454"; -} -.icon-book-favorite-1:before { - content: "\e455"; -} -.icon-bookmark-1-1:before { - content: "\e456"; -} -.icon-bookmark-2-1:before { - content: "\e457"; -} -.icon-bookmark-3-1:before { - content: "\e458"; -} -.icon-bookmark-4-1:before { - content: "\e459"; -} -.icon-books-1:before { - content: "\e45a"; -} -.icon-books-2:before { - content: "\e45b"; -} -.icon-books-3:before { - content: "\e45c"; -} -.icon-briefcase-1:before { - content: "\e45d"; -} -.icon-contact-book-1:before { - content: "\e45e"; -} -.icon-contact-book-2:before { - content: "\e45f"; -} -.icon-contact-book-3:before { - content: "\e460"; -} -.icon-contact-book-4:before { - content: "\e461"; -} -.icon-copyright:before { - content: "\e462"; -} -.icon-creative-commons:before { - content: "\e463"; -} -.icon-cube:before { - content: "\e464"; -} -.icon-data-filter:before { - content: "\e465"; -} -.icon-document-box-1:before { - content: "\e466"; -} -.icon-document-box-2:before { - content: "\e467"; -} -.icon-document-box-3:before { - content: "\e468"; -} -.icon-drawer-1:before { - content: "\e469"; -} -.icon-drawer-2:before { - content: "\e46a"; -} -.icon-drawer-3:before { - content: "\e46b"; -} -.icon-envelope:before { - content: "\e46c"; -} -.icon-favortie-book-2:before { - content: "\e46d"; -} -.icon-file:before { - content: "\e46e"; -} -.icon-files:before { - content: "\e46f"; -} -.icon-filter-1:before { - content: "\e470"; -} -.icon-filter-2:before { - content: "\e471"; -} -.icon-layers-1:before { - content: "\e472"; -} -.icon-list-1:before { - content: "\e473"; -} -.icon-list-2:before { - content: "\e474"; -} -.icon-newspaper-1:before { - content: "\e475"; -} -.icon-newspaper-2:before { - content: "\e476"; -} -.icon-registry-1:before { - content: "\e477"; -} -.icon-registry-2:before { - content: "\e478"; -} -.icon-shield-1:before { - content: "\e479"; -} -.icon-shield-2:before { - content: "\e47a"; -} -.icon-shield-3:before { - content: "\e47b"; -} -.icon-sketchbook:before { - content: "\e47c"; -} -.icon-sound-book:before { - content: "\e47d"; -} -.icon-thumbnails-1:before { - content: "\e47e"; -} -.icon-thumbnails-2:before { - content: "\e47f"; -} -.icon-hierarchy-1:before { - content: "\e480"; -} -.icon-hierarchy-2:before { - content: "\e481"; -} -.icon-hierarchy-3:before { - content: "\e482"; -} -.icon-hierarchy-4:before { - content: "\e483"; -} -.icon-hierarchy-5:before { - content: "\e484"; -} -.icon-hierarchy-6:before { - content: "\e485"; -} -.icon-hierarchy-7:before { - content: "\e486"; -} -.icon-hierarchy-8:before { - content: "\e487"; -} -.icon-network-1-1:before { - content: "\e488"; -} -.icon-network-2-1:before { - content: "\e489"; -} -.icon-backpack:before { - content: "\e48a"; -} -.icon-balance-1:before { - content: "\e48b"; -} -.icon-bed:before { - content: "\e48c"; -} -.icon-bench:before { - content: "\e48d"; -} -.icon-bomb-1:before { - content: "\e48e"; -} -.icon-bricks:before { - content: "\e48f"; -} -.icon-bullets:before { - content: "\e490"; -} -.icon-buoy-ring:before { - content: "\e491"; -} -.icon-campfire:before { - content: "\e492"; -} -.icon-can:before { - content: "\e493"; -} -.icon-candle:before { - content: "\e494"; -} -.icon-canon:before { - content: "\e495"; -} -.icon-cctv-1:before { - content: "\e496"; -} -.icon-cctv-2:before { - content: "\e497"; -} -.icon-chair:before { - content: "\e498"; -} -.icon-chair-director:before { - content: "\e499"; -} -.icon-cigarette:before { - content: "\e49a"; -} -.icon-construction-sign:before { - content: "\e49b"; -} -.icon-diamond:before { - content: "\e49c"; -} -.icon-disabled:before { - content: "\e49d"; -} -.icon-door:before { - content: "\e49e"; -} -.icon-drawer:before { - content: "\e49f"; -} -.icon-driller:before { - content: "\e4a0"; -} -.icon-dumbbell:before { - content: "\e4a1"; -} -.icon-fire-extinguisher:before { - content: "\e4a2"; -} -.icon-flashlight:before { - content: "\e4a3"; -} -.icon-gas-station:before { - content: "\e4a4"; -} -.icon-gun:before { - content: "\e4a5"; -} -.icon-lamp-1:before { - content: "\e4a6"; -} -.icon-lamp-2:before { - content: "\e4a7"; -} -.icon-lamp-3:before { - content: "\e4a8"; -} -.icon-lamp-4:before { - content: "\e4a9"; -} -.icon-lightbulb-1:before { - content: "\e4aa"; -} -.icon-lightbulb-2:before { - content: "\e4ab"; -} -.icon-measuring-tape:before { - content: "\e4ac"; -} -.icon-mine-cart:before { - content: "\e4ad"; -} -.icon-missile:before { - content: "\e4ae"; -} -.icon-ring:before { - content: "\e4af"; -} -.icon-scale-1:before { - content: "\e4b0"; -} -.icon-shovel:before { - content: "\e4b1"; -} -.icon-smoke-no:before { - content: "\e4b2"; -} -.icon-sofa-1:before { - content: "\e4b3"; -} -.icon-sofa-2:before { - content: "\e4b4"; -} -.icon-sofa-3:before { - content: "\e4b5"; -} -.icon-target:before { - content: "\e4b6"; -} -.icon-torch:before { - content: "\e4b7"; -} -.icon-traffic-cone:before { - content: "\e4b8"; -} -.icon-traffic-light-1:before { - content: "\e4b9"; -} -.icon-traffic-light-2:before { - content: "\e4ba"; -} -.icon-treasure-1:before { - content: "\e4bb"; -} -.icon-treasure-2:before { - content: "\e4bc"; -} -.icon-trowel:before { - content: "\e4bd"; -} -.icon-watering-can:before { - content: "\e4be"; -} -.icon-weigh:before { - content: "\e4bf"; -} -.icon-academic-cap:before { - content: "\e4c0"; -} -.icon-baseball-helmet:before { - content: "\e4c1"; -} -.icon-beanie:before { - content: "\e4c2"; -} -.icon-bike-helmet:before { - content: "\e4c3"; -} -.icon-bow:before { - content: "\e4c4"; -} -.icon-cap:before { - content: "\e4c5"; -} -.icon-chaplin:before { - content: "\e4c6"; -} -.icon-chef-hat:before { - content: "\e4c7"; -} -.icon-cloth-hanger:before { - content: "\e4c8"; -} -.icon-fins:before { - content: "\e4c9"; -} -.icon-football-helmet:before { - content: "\e4ca"; -} -.icon-glasses:before { - content: "\e4cb"; -} -.icon-glasses-1:before { - content: "\e4cc"; -} -.icon-glasses-2:before { - content: "\e4cd"; -} -.icon-magician-hat:before { - content: "\e4ce"; -} -.icon-monocle-1:before { - content: "\e4cf"; -} -.icon-monocle-2:before { - content: "\e4d0"; -} -.icon-necktie:before { - content: "\e4d1"; -} -.icon-polo-shirt:before { - content: "\e4d2"; -} -.icon-safety-helmet:before { - content: "\e4d3"; -} -.icon-scuba-tank:before { - content: "\e4d4"; -} -.icon-shirt-1:before { - content: "\e4d5"; -} -.icon-shirt-2:before { - content: "\e4d6"; -} -.icon-sneakers:before { - content: "\e4d7"; -} -.icon-snorkel:before { - content: "\e4d8"; -} -.icon-sombrero:before { - content: "\e4d9"; -} -.icon-sunglasses:before { - content: "\e4da"; -} -.icon-tall-hat:before { - content: "\e4db"; -} -.icon-trousers:before { - content: "\e4dc"; -} -.icon-walking-stick:before { - content: "\e4dd"; -} -.icon-arrow-redo:before { - content: "\e4de"; -} -.icon-arrow-undo:before { - content: "\e4df"; -} -.icon-bold:before { - content: "\e4e0"; -} -.icon-columns:before { - content: "\e4e1"; -} -.icon-eraser:before { - content: "\e4e2"; -} -.icon-font-color:before { - content: "\e4e3"; -} -.icon-html:before { - content: "\e4e4"; -} -.icon-italic:before { - content: "\e4e5"; -} -.icon-list-1-1:before { - content: "\e4e6"; -} -.icon-list-2-1:before { - content: "\e4e7"; -} -.icon-list-3:before { - content: "\e4e8"; -} -.icon-list-4:before { - content: "\e4e9"; -} -.icon-paragraph:before { - content: "\e4ea"; -} -.icon-paste:before { - content: "\e4eb"; -} -.icon-print-preview:before { - content: "\e4ec"; -} -.icon-quote:before { - content: "\e4ed"; -} -.icon-strikethrough:before { - content: "\e4ee"; -} -.icon-text:before { - content: "\e4ef"; -} -.icon-text-wrapping-1:before { - content: "\e4f0"; -} -.icon-text-wrapping-2:before { - content: "\e4f1"; -} -.icon-text-wrapping-3:before { - content: "\e4f2"; -} -.icon-underline:before { - content: "\e4f3"; -} -.icon-align-center:before { - content: "\e4f4"; -} -.icon-align-left:before { - content: "\e4f5"; -} -.icon-align-right:before { - content: "\e4f6"; -} -.icon-all-caps:before { - content: "\e4f7"; -} -.icon-arrange-2-1:before { - content: "\e4f8"; -} -.icon-arrange-2-2:before { - content: "\e4f9"; -} -.icon-arrange-2-3:before { - content: "\e4fa"; -} -.icon-arrange-2-4:before { - content: "\e4fb"; -} -.icon-arrange-3-1:before { - content: "\e4fc"; -} -.icon-arrange-3-2:before { - content: "\e4fd"; -} -.icon-arrange-3-3:before { - content: "\e4fe"; -} -.icon-arrange-3-4:before { - content: "\e4ff"; -} -.icon-arrange-3-5:before { - content: "\e500"; -} -.icon-arrange-4-1:before { - content: "\e501"; -} -.icon-arrange-4-2:before { - content: "\e502"; -} -.icon-arrange-4-3:before { - content: "\e503"; -} -.icon-arrange-5:before { - content: "\e504"; -} -.icon-consolidate-all:before { - content: "\e505"; -} -.icon-decrease-indent-1:before { - content: "\e506"; -} -.icon-decrease-indent-2:before { - content: "\e507"; -} -.icon-horizontal-page:before { - content: "\e508"; -} -.icon-increase-indent-1:before { - content: "\e509"; -} -.icon-increase-indent-2:before { - content: "\e50a"; -} -.icon-justify:before { - content: "\e50b"; -} -.icon-leading-1:before { - content: "\e50c"; -} -.icon-leading-2:before { - content: "\e50d"; -} -.icon-left-indent:before { - content: "\e50e"; -} -.icon-right-indent:before { - content: "\e50f"; -} -.icon-small-caps:before { - content: "\e510"; -} -.icon-vertical-page:before { - content: "\e511"; -} -.icon-alt-mac:before { - content: "\e512"; -} -.icon-alt-windows:before { - content: "\e513"; -} -.icon-arrow-down:before { - content: "\e514"; -} -.icon-arrow-down-left:before { - content: "\e515"; -} -.icon-arrow-down-right:before { - content: "\e516"; -} -.icon-arrow-left:before { - content: "\e517"; -} -.icon-arrow-right:before { - content: "\e518"; -} -.icon-arrow-up:before { - content: "\e519"; -} -.icon-arrow-up-left:before { - content: "\e51a"; -} -.icon-arrow-up-right:before { - content: "\e51b"; -} -.icon-asterisk-1:before { - content: "\e51c"; -} -.icon-asterisk-2:before { - content: "\e51d"; -} -.icon-back-tab-1:before { - content: "\e51e"; -} -.icon-back-tab-2:before { - content: "\e51f"; -} -.icon-backward-delete:before { - content: "\e520"; -} -.icon-blank:before { - content: "\e521"; -} -.icon-eject:before { - content: "\e522"; -} -.icon-enter-1:before { - content: "\e523"; -} -.icon-enter-2:before { - content: "\e524"; -} -.icon-escape:before { - content: "\e525"; -} -.icon-page-down:before { - content: "\e526"; -} -.icon-page-up:before { - content: "\e527"; -} -.icon-return-1:before { - content: "\e528"; -} -.icon-shift:before { - content: "\e529"; -} -.icon-shift-2:before { - content: "\e52a"; -} -.icon-tab:before { - content: "\e52b"; -} -.icon-apple:before { - content: "\e52c"; -} -.icon-beer:before { - content: "\e52d"; -} -.icon-boil:before { - content: "\e52e"; -} -.icon-bottle-1:before { - content: "\e52f"; -} -.icon-bottle-2:before { - content: "\e530"; -} -.icon-bottle-3:before { - content: "\e531"; -} -.icon-bottle-4:before { - content: "\e532"; -} -.icon-bread:before { - content: "\e533"; -} -.icon-burger-1:before { - content: "\e534"; -} -.icon-burger-2:before { - content: "\e535"; -} -.icon-cake-1:before { - content: "\e536"; -} -.icon-cake-2:before { - content: "\e537"; -} -.icon-champagne:before { - content: "\e538"; -} -.icon-cheese:before { - content: "\e539"; -} -.icon-cocktail-1:before { - content: "\e53a"; -} -.icon-cocktail-2:before { - content: "\e53b"; -} -.icon-cocktail-3:before { - content: "\e53c"; -} -.icon-coffee-cup:before { - content: "\e53d"; -} -.icon-coffee-cup-1:before { - content: "\e53e"; -} -.icon-coffee-pot:before { - content: "\e53f"; -} -.icon-deep-fry:before { - content: "\e540"; -} -.icon-energy-drink:before { - content: "\e541"; -} -.icon-espresso-machine:before { - content: "\e542"; -} -.icon-food-dome:before { - content: "\e543"; -} -.icon-fork-and-knife:before { - content: "\e544"; -} -.icon-fork-and-spoon:before { - content: "\e545"; -} -.icon-grape:before { - content: "\e546"; -} -.icon-grater:before { - content: "\e547"; -} -.icon-grill:before { - content: "\e548"; -} -.icon-hot-drinks-glass:before { - content: "\e549"; -} -.icon-hotdog:before { - content: "\e54a"; -} -.icon-ice-cream-1:before { - content: "\e54b"; -} -.icon-ice-cream-2:before { - content: "\e54c"; -} -.icon-ice-cream-3:before { - content: "\e54d"; -} -.icon-ice-drinks-glass:before { - content: "\e54e"; -} -.icon-juicer:before { - content: "\e54f"; -} -.icon-kitchen-timer:before { - content: "\e550"; -} -.icon-milk:before { - content: "\e551"; -} -.icon-orange:before { - content: "\e552"; -} -.icon-oven:before { - content: "\e553"; -} -.icon-pan-fry:before { - content: "\e554"; -} -.icon-pepper-salt:before { - content: "\e555"; -} -.icon-pizza:before { - content: "\e556"; -} -.icon-pop-corn:before { - content: "\e557"; -} -.icon-serving:before { - content: "\e558"; -} -.icon-soda:before { - content: "\e559"; -} -.icon-soda-can-1:before { - content: "\e55a"; -} -.icon-soda-can-2:before { - content: "\e55b"; -} -.icon-steam:before { - content: "\e55c"; -} -.icon-tea-pot:before { - content: "\e55d"; -} -.icon-thermometer-high:before { - content: "\e55e"; -} -.icon-thermometer-low:before { - content: "\e55f"; -} -.icon-thermometer-medium:before { - content: "\e560"; -} -.icon-water:before { - content: "\e561"; -} -.icon-wine:before { - content: "\e562"; -} -.icon-ambulance:before { - content: "\e563"; -} -.icon-beaker-1:before { - content: "\e564"; -} -.icon-beaker-2:before { - content: "\e565"; -} -.icon-blood:before { - content: "\e566"; -} -.icon-drug:before { - content: "\e567"; -} -.icon-first-aid:before { - content: "\e568"; -} -.icon-hashish:before { - content: "\e569"; -} -.icon-heart-pulse:before { - content: "\e56a"; -} -.icon-hospital-1:before { - content: "\e56b"; -} -.icon-hospital-2:before { - content: "\e56c"; -} -.icon-hospital-sign-1:before { - content: "\e56d"; -} -.icon-hospital-sign-2:before { - content: "\e56e"; -} -.icon-hospital-sign-3:before { - content: "\e56f"; -} -.icon-medicine:before { - content: "\e570"; -} -.icon-microscope:before { - content: "\e571"; -} -.icon-mortar-and-pestle:before { - content: "\e572"; -} -.icon-plaster:before { - content: "\e573"; -} -.icon-pulse-graph-1:before { - content: "\e574"; -} -.icon-pulse-graph-2:before { - content: "\e575"; -} -.icon-pulse-graph-3:before { - content: "\e576"; -} -.icon-red-cross:before { - content: "\e577"; -} -.icon-stethoscope:before { - content: "\e578"; -} -.icon-syringe:before { - content: "\e579"; -} -.icon-yin-yang:before { - content: "\e57a"; -} -.icon-balloon:before { - content: "\e57b"; -} -.icon-briefcase-lock:before { - content: "\e57c"; -} -.icon-card:before { - content: "\e57d"; -} -.icon-cards-1:before { - content: "\e57e"; -} -.icon-cards-2:before { - content: "\e57f"; -} -.icon-curtain:before { - content: "\e580"; -} -.icon-dice-1:before { - content: "\e581"; -} -.icon-dice-2:before { - content: "\e582"; -} -.icon-pacman:before { - content: "\e583"; -} -.icon-pacman-ghost:before { - content: "\e584"; -} -.icon-sign-1:before { - content: "\e585"; -} -.icon-smiley-happy:before { - content: "\e586"; -} -.icon-smiley-sad:before { - content: "\e587"; -} -.icon-smileys:before { - content: "\e588"; -} -.icon-suitcase-1:before { - content: "\e589"; -} -.icon-suitcase-2:before { - content: "\e58a"; -} -.icon-tetris:before { - content: "\e58b"; -} -.icon-ticket-1:before { - content: "\e58c"; -} -.icon-ticket-2:before { - content: "\e58d"; -} -.icon-ticket-3:before { - content: "\e58e"; -} -.icon-virus:before { - content: "\e58f"; -} -.icon-cloud-1:before { - content: "\e590"; -} -.icon-cloud-lightning:before { - content: "\e591"; -} -.icon-clouds:before { - content: "\e592"; -} -.icon-first-quarter-half-moon:before { - content: "\e593"; -} -.icon-full-moon:before { - content: "\e594"; -} -.icon-hail:before { - content: "\e595"; -} -.icon-heavy-rain:before { - content: "\e596"; -} -.icon-moon-cloud:before { - content: "\e597"; -} -.icon-rain:before { - content: "\e598"; -} -.icon-rain-lightning:before { - content: "\e599"; -} -.icon-snow:before { - content: "\e59a"; -} -.icon-sun:before { - content: "\e59b"; -} -.icon-sun-cloud:before { - content: "\e59c"; -} -.icon-thermometer:before { - content: "\e59d"; -} -.icon-third-quarter-half-moon:before { - content: "\e59e"; -} -.icon-umbrella:before { - content: "\e59f"; -} -.icon-waning-crescent-moon:before { - content: "\e5a0"; -} -.icon-waning-gibbous-moon:before { - content: "\e5a1"; -} -.icon-waxing-crescent-moon:before { - content: "\e5a2"; -} -.icon-waxing-gibbous-moon:before { - content: "\e5a3"; -} -.icon-bicycle:before { - content: "\e5a4"; -} -.icon-bus-1:before { - content: "\e5a5"; -} -.icon-bus-2:before { - content: "\e5a6"; -} -.icon-car-1:before { - content: "\e5a7"; -} -.icon-car-2:before { - content: "\e5a8"; -} -.icon-car-3:before { - content: "\e5a9"; -} -.icon-car-4:before { - content: "\e5aa"; -} -.icon-helicopter:before { - content: "\e5ab"; -} -.icon-mountain-bike:before { - content: "\e5ac"; -} -.icon-pickup:before { - content: "\e5ad"; -} -.icon-plane-1:before { - content: "\e5ae"; -} -.icon-plane-2:before { - content: "\e5af"; -} -.icon-plane-landing:before { - content: "\e5b0"; -} -.icon-plane-takeoff:before { - content: "\e5b1"; -} -.icon-road:before { - content: "\e5b2"; -} -.icon-road-bike:before { - content: "\e5b3"; -} -.icon-rocket:before { - content: "\e5b4"; -} -.icon-scooter:before { - content: "\e5b5"; -} -.icon-ship:before { - content: "\e5b6"; -} -.icon-train:before { - content: "\e5b7"; -} -.icon-tram:before { - content: "\e5b8"; -} -.icon-cactus:before { - content: "\e5b9"; -} -.icon-clover:before { - content: "\e5ba"; -} -.icon-flower:before { - content: "\e5bb"; -} -.icon-hand-eco:before { - content: "\e5bc"; -} -.icon-hand-globe:before { - content: "\e5bd"; -} -.icon-leaf:before { - content: "\e5be"; -} -.icon-light-eco:before { - content: "\e5bf"; -} -.icon-potted-plant-1:before { - content: "\e5c0"; -} -.icon-potted-plant-2:before { - content: "\e5c1"; -} -.icon-2-fingers-down-swipe:before { - content: "\e5c2"; -} -.icon-2-fingers-horizontal-swipe:before { - content: "\e5c3"; -} -.icon-2-fingers-left-swipe:before { - content: "\e5c4"; -} -.icon-2-fingers-omnidirectional-swipe:before { - content: "\e5c5"; -} -.icon-2-fingers-right-swipe:before { - content: "\e5c6"; -} -.icon-2-fingers-tab-hold:before { - content: "\e5c7"; -} -.icon-2-fingers-tap:before { - content: "\e5c8"; -} -.icon-2-fingers-up-swipe:before { - content: "\e5c9"; -} -.icon-2-fingers-vertical-swipe:before { - content: "\e5ca"; -} -.icon-2finger-double-tap:before { - content: "\e5cb"; -} -.icon-double-tap:before { - content: "\e5cc"; -} -.icon-drag-down:before { - content: "\e5cd"; -} -.icon-drag-horizontal:before { - content: "\e5ce"; -} -.icon-drag-left:before { - content: "\e5cf"; -} -.icon-drag-right:before { - content: "\e5d0"; -} -.icon-drag-up:before { - content: "\e5d1"; -} -.icon-drag-vertical:before { - content: "\e5d2"; -} -.icon-filck-down:before { - content: "\e5d3"; -} -.icon-flick-up:before { - content: "\e5d4"; -} -.icon-horizontal-flick:before { - content: "\e5d5"; -} -.icon-left-flick:before { - content: "\e5d6"; -} -.icon-omnidirectional-drag:before { - content: "\e5d7"; -} -.icon-omnidirectional-flick:before { - content: "\e5d8"; -} -.icon-omnidirectional-swipe:before { - content: "\e5d9"; -} -.icon-pinch:before { - content: "\e5da"; -} -.icon-right-flick:before { - content: "\e5db"; -} -.icon-rotate-clockwise:before { - content: "\e5dc"; -} -.icon-rotate-counterclockwise:before { - content: "\e5dd"; -} -.icon-spread:before { - content: "\e5de"; -} -.icon-swipe-down:before { - content: "\e5df"; -} -.icon-swipe-horizontal:before { - content: "\e5e0"; -} -.icon-swipe-left:before { - content: "\e5e1"; -} -.icon-swipe-right:before { - content: "\e5e2"; -} -.icon-swipe-up:before { - content: "\e5e3"; -} -.icon-swipe-vertical:before { - content: "\e5e4"; -} -.icon-tap:before { - content: "\e5e5"; -} -.icon-tap-hold:before { - content: "\e5e6"; -} -.icon-vertical-flick:before { - content: "\e5e7"; -} -.icon-arrow-1-1:before { - content: "\e5e8"; -} -.icon-arrow-2-1:before { - content: "\e5e9"; -} -.icon-arrow-3:before { - content: "\e5ea"; -} -.icon-arrow-4:before { - content: "\e5eb"; -} -.icon-arrow-5:before { - content: "\e5ec"; -} -.icon-arrow-6:before { - content: "\e5ed"; -} -.icon-arrow-7:before { - content: "\e5ee"; -} -.icon-arrow-8:before { - content: "\e5ef"; -} -.icon-arrow-9:before { - content: "\e5f0"; -} -.icon-arrow-10:before { - content: "\e5f1"; -} -.icon-arrow-11:before { - content: "\e5f2"; -} -.icon-arrow-12:before { - content: "\e5f3"; -} -.icon-arrow-13:before { - content: "\e5f4"; -} -.icon-arrow-14:before { - content: "\e5f5"; -} -.icon-arrow-15:before { - content: "\e5f6"; -} -.icon-arrow-16:before { - content: "\e5f7"; -} -.icon-arrow-17:before { - content: "\e5f8"; -} -.icon-arrow-18:before { - content: "\e5f9"; -} -.icon-arrow-19:before { - content: "\e5fa"; -} -.icon-arrow-20:before { - content: "\e5fb"; -} -.icon-arrow-21:before { - content: "\e5fc"; -} -.icon-arrow-22:before { - content: "\e5fd"; -} -.icon-arrow-23:before { - content: "\e5fe"; -} -.icon-arrow-24:before { - content: "\e5ff"; -} -.icon-arrow-25:before { - content: "\e600"; -} -.icon-arrow-26:before { - content: "\e601"; -} -.icon-arrow-27:before { - content: "\e602"; -} -.icon-arrow-28:before { - content: "\e603"; -} -.icon-arrow-29:before { - content: "\e604"; -} -.icon-arrow-30:before { - content: "\e605"; -} -.icon-arrow-31:before { - content: "\e606"; -} -.icon-arrow-32:before { - content: "\e607"; -} -.icon-arrow-33:before { - content: "\e608"; -} -.icon-arrow-34:before { - content: "\e609"; -} -.icon-arrow-35:before { - content: "\e60a"; -} -.icon-arrow-36:before { - content: "\e60b"; -} -.icon-arrow-37:before { - content: "\e60c"; -} -.icon-arrow-38:before { - content: "\e60d"; -} -.icon-arrow-39:before { - content: "\e60e"; -} -.icon-arrow-40:before { - content: "\e60f"; -} -.icon-arrow-41:before { - content: "\e610"; -} -.icon-arrow-42:before { - content: "\e611"; -} -.icon-arrow-43:before { - content: "\e612"; -} -.icon-arrow-44:before { - content: "\e613"; -} -.icon-arrow-45:before { - content: "\e614"; -} -.icon-arrow-46:before { - content: "\e615"; -} -.icon-arrow-47:before { - content: "\e616"; -} -.icon-arrow-48:before { - content: "\e617"; -} -.icon-arrow-49:before { - content: "\e618"; -} -.icon-arrow-50:before { - content: "\e619"; -} -.icon-arrow-51:before { - content: "\e61a"; -} -.icon-arrow-52:before { - content: "\e61b"; -} -.icon-arrow-53:before { - content: "\e61c"; -} -.icon-arrow-54:before { - content: "\e61d"; -} -.icon-arrow-55:before { - content: "\e61e"; -} -.icon-arrow-56:before { - content: "\e61f"; -} -.icon-arrow-57:before { - content: "\e620"; -} -.icon-arrow-58:before { - content: "\e621"; -} -.icon-arrow-59:before { - content: "\e622"; -} -.icon-arrow-60:before { - content: "\e623"; -} -.icon-arrow-61:before { - content: "\e624"; -} -.icon-arrow-62:before { - content: "\e625"; -} -.icon-arrow-63:before { - content: "\e626"; -} -.icon-arrow-64:before { - content: "\e627"; -} -.icon-arrow-65:before { - content: "\e628"; -} -.icon-arrow-66:before { - content: "\e629"; -} -.icon-arrow-67:before { - content: "\e62a"; -} -.icon-arrow-68:before { - content: "\e62b"; -} -.icon-arrow-69:before { - content: "\e62c"; -} -.icon-arrow-70:before { - content: "\e62d"; -} -.icon-arrow-71:before { - content: "\e62e"; -} -.icon-arrow-72:before { - content: "\e62f"; -} -.icon-arrow-circle-1:before { - content: "\e630"; -} -.icon-arrow-circle-2:before { - content: "\e631"; -} -.icon-arrow-circle-3:before { - content: "\e632"; -} -.icon-arrow-circle-4:before { - content: "\e633"; -} -.icon-arrow-circle-5:before { - content: "\e634"; -} -.icon-arrow-circle-6:before { - content: "\e635"; -} -.icon-arrow-circle-7:before { - content: "\e636"; -} -.icon-arrow-circle-8:before { - content: "\e637"; -} -.icon-arrow-circle-9:before { - content: "\e638"; -} -.icon-arrow-circle-10:before { - content: "\e639"; -} -.icon-arrow-circle-11:before { - content: "\e63a"; -} -.icon-arrow-circle-12:before { - content: "\e63b"; -} -.icon-arrow-circle-13:before { - content: "\e63c"; -} -.icon-arrow-circle-14:before { - content: "\e63d"; -} -.icon-arrow-circle-15:before { - content: "\e63e"; -} -.icon-arrow-circle-16:before { - content: "\e63f"; -} -.icon-arrow-circle-17:before { - content: "\e640"; -} -.icon-arrow-circle-18:before { - content: "\e641"; -} -.icon-arrow-circle-19:before { - content: "\e642"; -} -.icon-arrow-circle-20:before { - content: "\e643"; -} -.icon-arrow-circle-21:before { - content: "\e644"; -} -.icon-arrow-circle-22:before { - content: "\e645"; -} -.icon-arrow-circle-23:before { - content: "\e646"; -} -.icon-arrow-circle-24:before { - content: "\e647"; -} -.icon-arrow-circle-25:before { - content: "\e648"; -} -.icon-arrow-circle-26:before { - content: "\e649"; -} -.icon-arrow-circle-27:before { - content: "\e64a"; -} -.icon-arrow-circle-28:before { - content: "\e64b"; -} -.icon-arrow-circle-29:before { - content: "\e64c"; -} -.icon-arrow-circle-30:before { - content: "\e64d"; -} -.icon-arrow-delete-1:before { - content: "\e64e"; -} -.icon-arrow-delete-2:before { - content: "\e64f"; -} -.icon-arrow-dot-1:before { - content: "\e650"; -} -.icon-arrow-dot-2:before { - content: "\e651"; -} -.icon-arrow-dot-3:before { - content: "\e652"; -} -.icon-arrow-dot-4:before { - content: "\e653"; -} -.icon-arrow-dot-5:before { - content: "\e654"; -} -.icon-arrow-dot-6:before { - content: "\e655"; -} -.icon-arrow-rectangle-1:before { - content: "\e656"; -} -.icon-arrow-rectangle-2:before { - content: "\e657"; -} -.icon-arrow-rectangle-3:before { - content: "\e658"; -} -.icon-arrow-rectangle-4:before { - content: "\e659"; -} -.icon-arrow-rectangle-5:before { - content: "\e65a"; -} -.icon-arrow-rectangle-6:before { - content: "\e65b"; -} -.icon-arrow-rectangle-7:before { - content: "\e65c"; -} -.icon-arrow-rectangle-8:before { - content: "\e65d"; -} -.icon-arrow-rectangle-9:before { - content: "\e65e"; -} -.icon-arrow-rectangle-10:before { - content: "\e65f"; -} -.icon-arrow-rectangle-11:before { - content: "\e660"; -} -.icon-arrow-rectangle-12:before { - content: "\e661"; -} -.icon-arrow-rectangle-13:before { - content: "\e662"; -} -.icon-arrow-rectangle-14:before { - content: "\e663"; -} -.icon-arrow-rectangle-15:before { - content: "\e664"; -} -.icon-arrow-rectangle-16:before { - content: "\e665"; -} -.icon-arrow-rectangle-17:before { - content: "\e666"; -} -.icon-arrow-rectangle-18:before { - content: "\e667"; -} -.icon-arrow-rectangle-19:before { - content: "\e668"; -} -.icon-arrow-rectangle-20:before { - content: "\e669"; -} diff --git a/app/assets/stylesheets/boot/streamline-icons.scss.erb b/app/assets/stylesheets/boot/streamline-icons.scss.erb new file mode 100644 index 000000000..680509025 --- /dev/null +++ b/app/assets/stylesheets/boot/streamline-icons.scss.erb @@ -0,0 +1,4964 @@ +<% # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later %> +@charset "UTF-8"; + +$path: "/assets/Streamline"; + +@font-face { + font-family: "streamline-30px"; + src:url('<%= asset_path("Streamline/streamline-30px.woff") %>'); + font-weight: normal; + font-style: normal; +} + +[data-icon]:before { + font-family: "streamline-30px" !important; + content: attr(data-icon); + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "streamline-30px" !important; + font-style: normal !important; + font-weight: normal !important; + font-variant: normal !important; + text-transform: none !important; + speak: none; + line-height: 1; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-aim-1:before { + content: "\e000"; +} +.icon-aim-2:before { + content: "\e001"; +} +.icon-aim-3:before { + content: "\e002"; +} +.icon-bin-1:before { + content: "\e003"; +} +.icon-bin-2:before { + content: "\e004"; +} +.icon-binocular:before { + content: "\e005"; +} +.icon-bomb:before { + content: "\e006"; +} +.icon-clip-1:before { + content: "\e007"; +} +.icon-clip-2:before { + content: "\e008"; +} +.icon-cutter:before { + content: "\e009"; +} +.icon-delete-1:before { + content: "\e00a"; +} +.icon-delete-2:before { + content: "\e00b"; +} +.icon-edit-1:before { + content: "\e00c"; +} +.icon-edit-2:before { + content: "\e00d"; +} +.icon-edit-3:before { + content: "\e00e"; +} +.icon-hide:before { + content: "\e00f"; +} +.icon-ink:before { + content: "\e010"; +} +.icon-key-1:before { + content: "\e011"; +} +.icon-key-2:before { + content: "\e012"; +} +.icon-link-1:before { + content: "\e013"; +} +.icon-link-2:before { + content: "\e014"; +} +.icon-link-3:before { + content: "\e015"; +} +.icon-link-broken-1:before { + content: "\e016"; +} +.icon-link-broken-2:before { + content: "\e017"; +} +.icon-lock-1:before { + content: "\e018"; +} +.icon-lock-2:before { + content: "\e019"; +} +.icon-lock-3:before { + content: "\e01a"; +} +.icon-lock-4:before { + content: "\e01b"; +} +.icon-lock-5:before { + content: "\e01c"; +} +.icon-lock-unlock-1:before { + content: "\e01d"; +} +.icon-lock-unlock-2:before { + content: "\e01e"; +} +.icon-magnifier:before { + content: "\e01f"; +} +.icon-pen-1:before { + content: "\e020"; +} +.icon-pen-2:before { + content: "\e021"; +} +.icon-pen-3:before { + content: "\e022"; +} +.icon-pen-4:before { + content: "\e023"; +} +.icon-pencil-1:before { + content: "\e024"; +} +.icon-pencil-2:before { + content: "\e025"; +} +.icon-pencil-3:before { + content: "\e026"; +} +.icon-pin-1:before { + content: "\e027"; +} +.icon-pin-2:before { + content: "\e028"; +} +.icon-power-1:before { + content: "\e029"; +} +.icon-power-2:before { + content: "\e02a"; +} +.icon-preview-1:before { + content: "\e02b"; +} +.icon-preview-2:before { + content: "\e02c"; +} +.icon-scissor-1:before { + content: "\e02d"; +} +.icon-scissor-2:before { + content: "\e02e"; +} +.icon-skull-1:before { + content: "\e02f"; +} +.icon-skull-2:before { + content: "\e030"; +} +.icon-type-1:before { + content: "\e031"; +} +.icon-type-2:before { + content: "\e032"; +} +.icon-type-3:before { + content: "\e033"; +} +.icon-type-4:before { + content: "\e034"; +} +.icon-zoom-area:before { + content: "\e035"; +} +.icon-zoom-in:before { + content: "\e036"; +} +.icon-zoom-out:before { + content: "\e037"; +} +.icon-cursor-1:before { + content: "\e038"; +} +.icon-cursor-2:before { + content: "\e039"; +} +.icon-cursor-3:before { + content: "\e03a"; +} +.icon-cursor-6:before { + content: "\e03b"; +} +.icon-cursor-move:before { + content: "\e03c"; +} +.icon-cursor-select-area:before { + content: "\e03d"; +} +.icon-cursors:before { + content: "\e03e"; +} +.icon-hand:before { + content: "\e03f"; +} +.icon-hand-block:before { + content: "\e040"; +} +.icon-hand-grab-1:before { + content: "\e041"; +} +.icon-hand-grab-2:before { + content: "\e042"; +} +.icon-hand-point:before { + content: "\e043"; +} +.icon-hand-touch-1:before { + content: "\e044"; +} +.icon-hand-touch-2:before { + content: "\e045"; +} +.icon-hand-touch-3:before { + content: "\e046"; +} +.icon-hand-touch-4:before { + content: "\e047"; +} +.icon-bookmark-1:before { + content: "\e048"; +} +.icon-bookmark-2:before { + content: "\e049"; +} +.icon-bookmark-3:before { + content: "\e04a"; +} +.icon-bookmark-4:before { + content: "\e04b"; +} +.icon-tag-1:before { + content: "\e04c"; +} +.icon-tag-2:before { + content: "\e04d"; +} +.icon-tag-add:before { + content: "\e04e"; +} +.icon-tag-delete:before { + content: "\e04f"; +} +.icon-tags-1:before { + content: "\e050"; +} +.icon-tags-2:before { + content: "\e051"; +} +.icon-anchor-point-1:before { + content: "\e052"; +} +.icon-anchor-point-2:before { + content: "\e053"; +} +.icon-arrange-1:before { + content: "\e054"; +} +.icon-arrange-2:before { + content: "\e055"; +} +.icon-board:before { + content: "\e056"; +} +.icon-brush-1:before { + content: "\e057"; +} +.icon-brush-2:before { + content: "\e058"; +} +.icon-bucket:before { + content: "\e059"; +} +.icon-crop:before { + content: "\e05a"; +} +.icon-dropper-1:before { + content: "\e05b"; +} +.icon-dropper-2:before { + content: "\e05c"; +} +.icon-dropper-3:before { + content: "\e05d"; +} +.icon-glue:before { + content: "\e05e"; +} +.icon-grid:before { + content: "\e05f"; +} +.icon-layers:before { + content: "\e060"; +} +.icon-magic-wand-1:before { + content: "\e061"; +} +.icon-magic-wand-2:before { + content: "\e062"; +} +.icon-magnet:before { + content: "\e063"; +} +.icon-marker:before { + content: "\e064"; +} +.icon-palette:before { + content: "\e065"; +} +.icon-pen-5:before { + content: "\e066"; +} +.icon-pen-6:before { + content: "\e067"; +} +.icon-quill:before { + content: "\e068"; +} +.icon-reflect:before { + content: "\e069"; +} +.icon-roller:before { + content: "\e06a"; +} +.icon-ruler-1:before { + content: "\e06b"; +} +.icon-ruler-2:before { + content: "\e06c"; +} +.icon-scale-diagonal-1:before { + content: "\e06d"; +} +.icon-scale-diagonal-2:before { + content: "\e06e"; +} +.icon-scale-horizontal:before { + content: "\e06f"; +} +.icon-scale-tool-1:before { + content: "\e070"; +} +.icon-scale-tool-2:before { + content: "\e071"; +} +.icon-scale-tool-3:before { + content: "\e072"; +} +.icon-scale-vertical:before { + content: "\e073"; +} +.icon-shear-tool:before { + content: "\e074"; +} +.icon-spray:before { + content: "\e075"; +} +.icon-stamp:before { + content: "\e076"; +} +.icon-stationery-1:before { + content: "\e077"; +} +.icon-stationery-2:before { + content: "\e078"; +} +.icon-stationery-3:before { + content: "\e079"; +} +.icon-vector:before { + content: "\e07a"; +} +.icon-award-1:before { + content: "\e07b"; +} +.icon-award-2:before { + content: "\e07c"; +} +.icon-award-3:before { + content: "\e07d"; +} +.icon-award-4:before { + content: "\e07e"; +} +.icon-award-5:before { + content: "\e07f"; +} +.icon-award-6:before { + content: "\e080"; +} +.icon-crown-1:before { + content: "\e081"; +} +.icon-crown-2:before { + content: "\e082"; +} +.icon-crown-3:before { + content: "\e083"; +} +.icon-fire:before { + content: "\e084"; +} +.icon-flag-1:before { + content: "\e085"; +} +.icon-flag-2:before { + content: "\e086"; +} +.icon-flag-3:before { + content: "\e087"; +} +.icon-flag-4:before { + content: "\e088"; +} +.icon-flag-5:before { + content: "\e089"; +} +.icon-flag-6:before { + content: "\e08a"; +} +.icon-flag-7:before { + content: "\e08b"; +} +.icon-flag-8:before { + content: "\e08c"; +} +.icon-google-plus-1:before { + content: "\e08d"; +} +.icon-google-plus-2:before { + content: "\e08e"; +} +.icon-hand-like-1:before { + content: "\e08f"; +} +.icon-hand-like-2:before { + content: "\e090"; +} +.icon-hand-unlike-1:before { + content: "\e091"; +} +.icon-hand-unlike-2:before { + content: "\e092"; +} +.icon-heart-1:before { + content: "\e093"; +} +.icon-heart-2:before { + content: "\e094"; +} +.icon-heart-angel:before { + content: "\e095"; +} +.icon-heart-broken:before { + content: "\e096"; +} +.icon-heart-minus:before { + content: "\e097"; +} +.icon-heart-plus:before { + content: "\e098"; +} +.icon-present:before { + content: "\e099"; +} +.icon-rank-1:before { + content: "\e09a"; +} +.icon-rank-2:before { + content: "\e09b"; +} +.icon-ribbon:before { + content: "\e09c"; +} +.icon-star-1:before { + content: "\e09d"; +} +.icon-star-2:before { + content: "\e09e"; +} +.icon-star-3:before { + content: "\e09f"; +} +.icon-star-4:before { + content: "\e0a0"; +} +.icon-star-5:before { + content: "\e0a1"; +} +.icon-star-6:before { + content: "\e0a2"; +} +.icon-star-7:before { + content: "\e0a3"; +} +.icon-star-8:before { + content: "\e0a4"; +} +.icon-star-9:before { + content: "\e0a5"; +} +.icon-star-10:before { + content: "\e0a6"; +} +.icon-trophy:before { + content: "\e0a7"; +} +.icon-baloon:before { + content: "\e0a8"; +} +.icon-bubble-1:before { + content: "\e0a9"; +} +.icon-bubble-2:before { + content: "\e0aa"; +} +.icon-bubble-add-1:before { + content: "\e0ab"; +} +.icon-bubble-add-2:before { + content: "\e0ac"; +} +.icon-bubble-add-3:before { + content: "\e0ad"; +} +.icon-bubble-ask-1:before { + content: "\e0ae"; +} +.icon-bubble-ask-2:before { + content: "\e0af"; +} +.icon-bubble-attention-2:before { + content: "\e0b0"; +} +.icon-bubble-attention-3:before { + content: "\e0b1"; +} +.icon-bubble-attention-4:before { + content: "\e0b2"; +} +.icon-bubble-attention-6:before { + content: "\e0b3"; +} +.icon-bubble-attention-7:before { + content: "\e0b4"; +} +.icon-bubble-block-1:before { + content: "\e0b5"; +} +.icon-bubble-block-2:before { + content: "\e0b6"; +} +.icon-bubble-block-3:before { + content: "\e0b7"; +} +.icon-bubble-chat-1:before { + content: "\e0b8"; +} +.icon-bubble-chat-2:before { + content: "\e0b9"; +} +.icon-bubble-check-1:before { + content: "\e0ba"; +} +.icon-bubble-check-2:before { + content: "\e0bb"; +} +.icon-bubble-check-3:before { + content: "\e0bc"; +} +.icon-bubble-comment-1:before { + content: "\e0bd"; +} +.icon-bubble-comment-2:before { + content: "\e0be"; +} +.icon-bubble-conversation-1:before { + content: "\e0bf"; +} +.icon-bubble-conversation-2:before { + content: "\e0c0"; +} +.icon-bubble-conversation-3:before { + content: "\e0c1"; +} +.icon-bubble-conversation-4:before { + content: "\e0c2"; +} +.icon-bubble-conversation-5:before { + content: "\e0c3"; +} +.icon-bubble-conversation-6:before { + content: "\e0c4"; +} +.icon-bubble-delete-1:before { + content: "\e0c5"; +} +.icon-bubble-delete-2:before { + content: "\e0c6"; +} +.icon-bubble-delete-3:before { + content: "\e0c7"; +} +.icon-bubble-edit-1:before { + content: "\e0c8"; +} +.icon-bubble-edit-2:before { + content: "\e0c9"; +} +.icon-bubble-edit-3:before { + content: "\e0ca"; +} +.icon-bubble-heart-1:before { + content: "\e0cb"; +} +.icon-bubble-heart-2:before { + content: "\e0cc"; +} +.icon-bubble-information:before { + content: "\e0cd"; +} +.icon-bubble-information-1:before { + content: "\e0ce"; +} +.icon-bubble-minus-1:before { + content: "\e0cf"; +} +.icon-bubble-minus-2:before { + content: "\e0d0"; +} +.icon-bubble-minus-3:before { + content: "\e0d1"; +} +.icon-bubble-quote-1:before { + content: "\e0d2"; +} +.icon-bubble-quote-2:before { + content: "\e0d3"; +} +.icon-bubble-smiley-1:before { + content: "\e0d4"; +} +.icon-bubble-smiley-2:before { + content: "\e0d5"; +} +.icon-bubble-smiley-3:before { + content: "\e0d6"; +} +.icon-bubble-smiley-4:before { + content: "\e0d7"; +} +.icon-bubble-star-1:before { + content: "\e0d8"; +} +.icon-bubble-star-2:before { + content: "\e0d9"; +} +.icon-bubble-star-3:before { + content: "\e0da"; +} +.icon-chat-1:before { + content: "\e0db"; +} +.icon-chat-2:before { + content: "\e0dc"; +} +.icon-chat-3:before { + content: "\e0dd"; +} +.icon-chat-4:before { + content: "\e0de"; +} +.icon-chat-5:before { + content: "\e0df"; +} +.icon-chat-6:before { + content: "\e0e0"; +} +.icon-chat-7:before { + content: "\e0e1"; +} +.icon-smiley-happy-1:before { + content: "\e0e2"; +} +.icon-smiley-happy-2:before { + content: "\e0e3"; +} +.icon-smiley-happy-3:before { + content: "\e0e4"; +} +.icon-smiley-happy-4:before { + content: "\e0e5"; +} +.icon-smiley-happy-5:before { + content: "\e0e6"; +} +.icon-smiley-sad-1:before { + content: "\e0e7"; +} +.icon-smiley-surprise:before { + content: "\e0e8"; +} +.icon-smiley-wink:before { + content: "\e0e9"; +} +.icon-call-1:before { + content: "\e0ea"; +} +.icon-call-2:before { + content: "\e0eb"; +} +.icon-call-3:before { + content: "\e0ec"; +} +.icon-call-4:before { + content: "\e0ed"; +} +.icon-call-add:before { + content: "\e0ee"; +} +.icon-call-block:before { + content: "\e0ef"; +} +.icon-call-delete:before { + content: "\e0f0"; +} +.icon-call-in:before { + content: "\e0f1"; +} +.icon-call-minus:before { + content: "\e0f2"; +} +.icon-call-out:before { + content: "\e0f3"; +} +.icon-contact:before { + content: "\e0f4"; +} +.icon-fax:before { + content: "\e0f5"; +} +.icon-hang-up:before { + content: "\e0f6"; +} +.icon-message:before { + content: "\e0f7"; +} +.icon-mobile-phone-1:before { + content: "\e0f8"; +} +.icon-mobile-phone-2:before { + content: "\e0f9"; +} +.icon-phone-1:before { + content: "\e0fa"; +} +.icon-phone-2:before { + content: "\e0fb"; +} +.icon-phone-3:before { + content: "\e0fc"; +} +.icon-phone-4:before { + content: "\e0fd"; +} +.icon-phone-vibration:before { + content: "\e0fe"; +} +.icon-signal-fine:before { + content: "\e0ff"; +} +.icon-signal-full:before { + content: "\e100"; +} +.icon-signal-high:before { + content: "\e101"; +} +.icon-signal-no:before { + content: "\e102"; +} +.icon-signal-poor:before { + content: "\e103"; +} +.icon-signal-weak:before { + content: "\e104"; +} +.icon-smartphone:before { + content: "\e105"; +} +.icon-tape:before { + content: "\e106"; +} +.icon-camera-symbol-1:before { + content: "\e107"; +} +.icon-camera-symbol-2:before { + content: "\e108"; +} +.icon-camera-symbol-3:before { + content: "\e109"; +} +.icon-headphone:before { + content: "\e10a"; +} +.icon-antenna-1:before { + content: "\e10b"; +} +.icon-antenna-2:before { + content: "\e10c"; +} +.icon-antenna-3:before { + content: "\e10d"; +} +.icon-hotspot-1:before { + content: "\e10e"; +} +.icon-hotspot-2:before { + content: "\e10f"; +} +.icon-link:before { + content: "\e110"; +} +.icon-megaphone-1:before { + content: "\e111"; +} +.icon-megaphone-2:before { + content: "\e112"; +} +.icon-radar:before { + content: "\e113"; +} +.icon-rss-1:before { + content: "\e114"; +} +.icon-rss-2:before { + content: "\e115"; +} +.icon-satellite:before { + content: "\e116"; +} +.icon-address-1:before { + content: "\e117"; +} +.icon-address-2:before { + content: "\e118"; +} +.icon-address-3:before { + content: "\e119"; +} +.icon-forward:before { + content: "\e11a"; +} +.icon-inbox-1:before { + content: "\e11b"; +} +.icon-inbox-2:before { + content: "\e11c"; +} +.icon-inbox-3:before { + content: "\e11d"; +} +.icon-inbox-4:before { + content: "\e11e"; +} +.icon-letter-1:before { + content: "\e11f"; +} +.icon-letter-2:before { + content: "\e120"; +} +.icon-letter-3:before { + content: "\e121"; +} +.icon-letter-4:before { + content: "\e122"; +} +.icon-letter-5:before { + content: "\e123"; +} +.icon-mail-1:before { + content: "\e124"; +} +.icon-mail-2:before { + content: "\e125"; +} +.icon-mail-add:before { + content: "\e126"; +} +.icon-mail-attention:before { + content: "\e127"; +} +.icon-mail-block:before { + content: "\e128"; +} +.icon-mail-box-1:before { + content: "\e129"; +} +.icon-mail-box-2:before { + content: "\e12a"; +} +.icon-mail-box-3:before { + content: "\e12b"; +} +.icon-mail-checked:before { + content: "\e12c"; +} +.icon-mail-compose:before { + content: "\e12d"; +} +.icon-mail-delete:before { + content: "\e12e"; +} +.icon-mail-favorite:before { + content: "\e12f"; +} +.icon-mail-inbox:before { + content: "\e130"; +} +.icon-mail-lock:before { + content: "\e131"; +} +.icon-mail-minus:before { + content: "\e132"; +} +.icon-mail-read:before { + content: "\e133"; +} +.icon-mail-recieved-1:before { + content: "\e134"; +} +.icon-mail-recieved-2:before { + content: "\e135"; +} +.icon-mail-search-1:before { + content: "\e136"; +} +.icon-mail-search-2:before { + content: "\e137"; +} +.icon-mail-sent-1:before { + content: "\e138"; +} +.icon-mail-sent-2:before { + content: "\e139"; +} +.icon-mail-setting:before { + content: "\e13a"; +} +.icon-mail-star:before { + content: "\e13b"; +} +.icon-mail-sync:before { + content: "\e13c"; +} +.icon-mail-time:before { + content: "\e13d"; +} +.icon-outbox-1:before { + content: "\e13e"; +} +.icon-outbox-2:before { + content: "\e13f"; +} +.icon-plane-paper-1:before { + content: "\e140"; +} +.icon-plane-paper-2:before { + content: "\e141"; +} +.icon-reply-mail-1:before { + content: "\e142"; +} +.icon-reply-mail-2:before { + content: "\e143"; +} +.icon-connection-1:before { + content: "\e144"; +} +.icon-connection-2:before { + content: "\e145"; +} +.icon-connection-3:before { + content: "\e146"; +} +.icon-contacts-1:before { + content: "\e147"; +} +.icon-contacts-2:before { + content: "\e148"; +} +.icon-contacts-3:before { + content: "\e149"; +} +.icon-contacts-4:before { + content: "\e14a"; +} +.icon-female:before { + content: "\e14b"; +} +.icon-gender:before { + content: "\e14c"; +} +.icon-gender-female:before { + content: "\e14d"; +} +.icon-gender-male:before { + content: "\e14e"; +} +.icon-id-1:before { + content: "\e14f"; +} +.icon-id-2:before { + content: "\e150"; +} +.icon-id-3:before { + content: "\e151"; +} +.icon-id-4:before { + content: "\e152"; +} +.icon-id-5:before { + content: "\e153"; +} +.icon-id-6:before { + content: "\e154"; +} +.icon-id-7:before { + content: "\e155"; +} +.icon-id-8:before { + content: "\e156"; +} +.icon-male:before { + content: "\e157"; +} +.icon-profile-1:before { + content: "\e158"; +} +.icon-profile-2:before { + content: "\e159"; +} +.icon-profile-3:before { + content: "\e15a"; +} +.icon-profile-4:before { + content: "\e15b"; +} +.icon-profile-5:before { + content: "\e15c"; +} +.icon-profile-6:before { + content: "\e15d"; +} +.icon-profile-athlete:before { + content: "\e15e"; +} +.icon-profile-bussiness-man:before { + content: "\e15f"; +} +.icon-profile-cook:before { + content: "\e160"; +} +.icon-profile-cop:before { + content: "\e161"; +} +.icon-profile-doctor-1:before { + content: "\e162"; +} +.icon-profile-doctor-2:before { + content: "\e163"; +} +.icon-profile-gentleman-1:before { + content: "\e164"; +} +.icon-profile-gentleman-2:before { + content: "\e165"; +} +.icon-profile-graduate:before { + content: "\e166"; +} +.icon-profile-king:before { + content: "\e167"; +} +.icon-profile-lady-1:before { + content: "\e168"; +} +.icon-profile-lady-2:before { + content: "\e169"; +} +.icon-profile-man:before { + content: "\e16a"; +} +.icon-profile-nurse-1:before { + content: "\e16b"; +} +.icon-profile-nurse-2:before { + content: "\e16c"; +} +.icon-profile-prisoner:before { + content: "\e16d"; +} +.icon-profile-serviceman-1:before { + content: "\e16e"; +} +.icon-profile-serviceman-2:before { + content: "\e16f"; +} +.icon-profile-spy:before { + content: "\e170"; +} +.icon-profile-teacher:before { + content: "\e171"; +} +.icon-profile-thief:before { + content: "\e172"; +} +.icon-user-1:before { + content: "\e173"; +} +.icon-user-2:before { + content: "\e174"; +} +.icon-user-add-1:before { + content: "\e175"; +} +.icon-user-add-2:before { + content: "\e176"; +} +.icon-user-block-1:before { + content: "\e177"; +} +.icon-user-block-2:before { + content: "\e178"; +} +.icon-user-checked-1:before { + content: "\e179"; +} +.icon-user-checked-2:before { + content: "\e17a"; +} +.icon-user-delete-1:before { + content: "\e17b"; +} +.icon-user-delete-2:before { + content: "\e17c"; +} +.icon-user-edit-1:before { + content: "\e17d"; +} +.icon-user-edit-2:before { + content: "\e17e"; +} +.icon-user-heart-1:before { + content: "\e17f"; +} +.icon-user-heart-2:before { + content: "\e180"; +} +.icon-user-lock:before { + content: "\e181"; +} +.icon-user-lock-1:before { + content: "\e182"; +} +.icon-user-minus-1:before { + content: "\e183"; +} +.icon-user-minus-2:before { + content: "\e184"; +} +.icon-user-search-1:before { + content: "\e185"; +} +.icon-user-search-2:before { + content: "\e186"; +} +.icon-user-setting-1:before { + content: "\e187"; +} +.icon-user-setting-2:before { + content: "\e188"; +} +.icon-user-star-1:before { + content: "\e189"; +} +.icon-user-star-2:before { + content: "\e18a"; +} +.icon-basket-1:before { + content: "\e18b"; +} +.icon-basket-2:before { + content: "\e18c"; +} +.icon-basket-3:before { + content: "\e18d"; +} +.icon-basket-add:before { + content: "\e18e"; +} +.icon-basket-minus:before { + content: "\e18f"; +} +.icon-briefcase-2:before { + content: "\e190"; +} +.icon-cart-1:before { + content: "\e191"; +} +.icon-cart-2:before { + content: "\e192"; +} +.icon-cart-3:before { + content: "\e193"; +} +.icon-cart-4:before { + content: "\e194"; +} +.icon-cut:before { + content: "\e195"; +} +.icon-hand-bag-1:before { + content: "\e196"; +} +.icon-hand-bag-2:before { + content: "\e197"; +} +.icon-purse-1:before { + content: "\e198"; +} +.icon-purse-2:before { + content: "\e199"; +} +.icon-qr-code:before { + content: "\e19a"; +} +.icon-receipt-1:before { + content: "\e19b"; +} +.icon-receipt-2:before { + content: "\e19c"; +} +.icon-receipt-3:before { + content: "\e19d"; +} +.icon-receipt-4:before { + content: "\e19e"; +} +.icon-shopping-1:before { + content: "\e19f"; +} +.icon-shopping-bag-1:before { + content: "\e1a0"; +} +.icon-shopping-bag-2:before { + content: "\e1a1"; +} +.icon-shopping-bag-3:before { + content: "\e1a2"; +} +.icon-sign-new-1:before { + content: "\e1a3"; +} +.icon-sign-new-2:before { + content: "\e1a4"; +} +.icon-sign-park:before { + content: "\e1a5"; +} +.icon-sign-star:before { + content: "\e1a6"; +} +.icon-trolley-1:before { + content: "\e1a7"; +} +.icon-trolley-2:before { + content: "\e1a8"; +} +.icon-trolley-3:before { + content: "\e1a9"; +} +.icon-trolley-load:before { + content: "\e1aa"; +} +.icon-trolley-off:before { + content: "\e1ab"; +} +.icon-wallet-1:before { + content: "\e1ac"; +} +.icon-wallet-2:before { + content: "\e1ad"; +} +.icon-wallet-3:before { + content: "\e1ae"; +} +.icon-camera-1:before { + content: "\e1af"; +} +.icon-camera-2:before { + content: "\e1b0"; +} +.icon-camera-3:before { + content: "\e1b1"; +} +.icon-camera-4:before { + content: "\e1b2"; +} +.icon-camera-5:before { + content: "\e1b3"; +} +.icon-camera-back:before { + content: "\e1b4"; +} +.icon-camera-focus:before { + content: "\e1b5"; +} +.icon-camera-frames:before { + content: "\e1b6"; +} +.icon-camera-front:before { + content: "\e1b7"; +} +.icon-camera-graph-1:before { + content: "\e1b8"; +} +.icon-camera-graph-2:before { + content: "\e1b9"; +} +.icon-camera-landscape:before { + content: "\e1ba"; +} +.icon-camera-lens-1:before { + content: "\e1bb"; +} +.icon-camera-lens-2:before { + content: "\e1bc"; +} +.icon-camera-light:before { + content: "\e1bd"; +} +.icon-camera-portrait:before { + content: "\e1be"; +} +.icon-camera-view:before { + content: "\e1bf"; +} +.icon-film-1:before { + content: "\e1c0"; +} +.icon-film-2:before { + content: "\e1c1"; +} +.icon-photo-1:before { + content: "\e1c2"; +} +.icon-photo-2:before { + content: "\e1c3"; +} +.icon-photo-frame:before { + content: "\e1c4"; +} +.icon-photos-1:before { + content: "\e1c5"; +} +.icon-photos-2:before { + content: "\e1c6"; +} +.icon-polaroid:before { + content: "\e1c7"; +} +.icon-signal-camera-1:before { + content: "\e1c8"; +} +.icon-signal-camera-2:before { + content: "\e1c9"; +} +.icon-user-photo:before { + content: "\e1ca"; +} +.icon-backward-1:before { + content: "\e1cb"; +} +.icon-dvd-player:before { + content: "\e1cc"; +} +.icon-eject-1:before { + content: "\e1cd"; +} +.icon-film-3:before { + content: "\e1ce"; +} +.icon-forward-1:before { + content: "\e1cf"; +} +.icon-handy-cam:before { + content: "\e1d0"; +} +.icon-movie-play-1:before { + content: "\e1d1"; +} +.icon-movie-play-2:before { + content: "\e1d2"; +} +.icon-movie-play-3:before { + content: "\e1d3"; +} +.icon-next-1:before { + content: "\e1d4"; +} +.icon-pause-1:before { + content: "\e1d5"; +} +.icon-play-1:before { + content: "\e1d6"; +} +.icon-player:before { + content: "\e1d7"; +} +.icon-previous-1:before { + content: "\e1d8"; +} +.icon-record-1:before { + content: "\e1d9"; +} +.icon-slate:before { + content: "\e1da"; +} +.icon-stop-1:before { + content: "\e1db"; +} +.icon-television:before { + content: "\e1dc"; +} +.icon-video-camera-1:before { + content: "\e1dd"; +} +.icon-video-camera-2:before { + content: "\e1de"; +} +.icon-backward-2:before { + content: "\e1df"; +} +.icon-cd:before { + content: "\e1e0"; +} +.icon-eject-2:before { + content: "\e1e1"; +} +.icon-equalizer-1:before { + content: "\e1e2"; +} +.icon-equalizer-2:before { + content: "\e1e3"; +} +.icon-forward-2:before { + content: "\e1e4"; +} +.icon-gramophone:before { + content: "\e1e5"; +} +.icon-gramophone-record:before { + content: "\e1e6"; +} +.icon-guitar:before { + content: "\e1e7"; +} +.icon-headphone-1:before { + content: "\e1e8"; +} +.icon-headphone-2:before { + content: "\e1e9"; +} +.icon-microphone-1:before { + content: "\e1ea"; +} +.icon-microphone-2:before { + content: "\e1eb"; +} +.icon-microphone-3:before { + content: "\e1ec"; +} +.icon-movie-play-4:before { + content: "\e1ed"; +} +.icon-music-note-1:before { + content: "\e1ee"; +} +.icon-music-note-3:before { + content: "\e1ef"; +} +.icon-music-note-4:before { + content: "\e1f0"; +} +.icon-music-note-5:before { + content: "\e1f1"; +} +.icon-next-2:before { + content: "\e1f2"; +} +.icon-notes-1:before { + content: "\e1f3"; +} +.icon-notes-2:before { + content: "\e1f4"; +} +.icon-pause-2:before { + content: "\e1f5"; +} +.icon-piano:before { + content: "\e1f6"; +} +.icon-play-2:before { + content: "\e1f7"; +} +.icon-playlist:before { + content: "\e1f8"; +} +.icon-previous-2:before { + content: "\e1f9"; +} +.icon-radio-1:before { + content: "\e1fa"; +} +.icon-radio-2:before { + content: "\e1fb"; +} +.icon-record-2:before { + content: "\e1fc"; +} +.icon-recorder:before { + content: "\e1fd"; +} +.icon-saxophone:before { + content: "\e1fe"; +} +.icon-speaker-1:before { + content: "\e1ff"; +} +.icon-speaker-2:before { + content: "\e200"; +} +.icon-speaker-3:before { + content: "\e201"; +} +.icon-stop-2:before { + content: "\e202"; +} +.icon-tape-1:before { + content: "\e203"; +} +.icon-trumpet:before { + content: "\e204"; +} +.icon-volume-down-1:before { + content: "\e205"; +} +.icon-volume-down-2:before { + content: "\e206"; +} +.icon-volume-loud-1:before { + content: "\e207"; +} +.icon-volume-loud-2:before { + content: "\e208"; +} +.icon-volume-low-1:before { + content: "\e209"; +} +.icon-volume-low-2:before { + content: "\e20a"; +} +.icon-volume-medium-1:before { + content: "\e20b"; +} +.icon-volume-medium-2:before { + content: "\e20c"; +} +.icon-volume-mute-1:before { + content: "\e20d"; +} +.icon-volume-mute-2:before { + content: "\e20e"; +} +.icon-volume-mute-3:before { + content: "\e20f"; +} +.icon-volume-up-1:before { + content: "\e210"; +} +.icon-volume-up-2:before { + content: "\e211"; +} +.icon-walkman:before { + content: "\e212"; +} +.icon-cloud:before { + content: "\e213"; +} +.icon-cloud-add:before { + content: "\e214"; +} +.icon-cloud-checked:before { + content: "\e215"; +} +.icon-cloud-delete:before { + content: "\e216"; +} +.icon-cloud-download:before { + content: "\e217"; +} +.icon-cloud-minus:before { + content: "\e218"; +} +.icon-cloud-refresh:before { + content: "\e219"; +} +.icon-cloud-sync:before { + content: "\e21a"; +} +.icon-cloud-upload:before { + content: "\e21b"; +} +.icon-download-1:before { + content: "\e21c"; +} +.icon-download-2:before { + content: "\e21d"; +} +.icon-download-3:before { + content: "\e21e"; +} +.icon-download-4:before { + content: "\e21f"; +} +.icon-download-5:before { + content: "\e220"; +} +.icon-download-6:before { + content: "\e221"; +} +.icon-download-7:before { + content: "\e222"; +} +.icon-download-8:before { + content: "\e223"; +} +.icon-download-9:before { + content: "\e224"; +} +.icon-download-10:before { + content: "\e225"; +} +.icon-download-11:before { + content: "\e226"; +} +.icon-download-12:before { + content: "\e227"; +} +.icon-download-13:before { + content: "\e228"; +} +.icon-download-14:before { + content: "\e229"; +} +.icon-download-15:before { + content: "\e22a"; +} +.icon-download-file:before { + content: "\e22b"; +} +.icon-download-folder:before { + content: "\e22c"; +} +.icon-goal-1:before { + content: "\e22d"; +} +.icon-goal-2:before { + content: "\e22e"; +} +.icon-transfer-1:before { + content: "\e22f"; +} +.icon-transfer-2:before { + content: "\e230"; +} +.icon-transfer-3:before { + content: "\e231"; +} +.icon-transfer-4:before { + content: "\e232"; +} +.icon-transfer-5:before { + content: "\e233"; +} +.icon-transfer-6:before { + content: "\e234"; +} +.icon-transfer-7:before { + content: "\e235"; +} +.icon-transfer-8:before { + content: "\e236"; +} +.icon-transfer-9:before { + content: "\e237"; +} +.icon-transfer-10:before { + content: "\e238"; +} +.icon-transfer-11:before { + content: "\e239"; +} +.icon-transfer-12:before { + content: "\e23a"; +} +.icon-upload-1:before { + content: "\e23b"; +} +.icon-upload-2:before { + content: "\e23c"; +} +.icon-upload-3:before { + content: "\e23d"; +} +.icon-upload-4:before { + content: "\e23e"; +} +.icon-upload-5:before { + content: "\e23f"; +} +.icon-upload-6:before { + content: "\e240"; +} +.icon-upload-7:before { + content: "\e241"; +} +.icon-upload-8:before { + content: "\e242"; +} +.icon-upload-9:before { + content: "\e243"; +} +.icon-upload-10:before { + content: "\e244"; +} +.icon-upload-11:before { + content: "\e245"; +} +.icon-upload-12:before { + content: "\e246"; +} +.icon-clipboard-1:before { + content: "\e247"; +} +.icon-clipboard-2:before { + content: "\e248"; +} +.icon-clipboard-3:before { + content: "\e249"; +} +.icon-clipboard-add:before { + content: "\e24a"; +} +.icon-clipboard-block:before { + content: "\e24b"; +} +.icon-clipboard-checked:before { + content: "\e24c"; +} +.icon-clipboard-delete:before { + content: "\e24d"; +} +.icon-clipboard-edit:before { + content: "\e24e"; +} +.icon-clipboard-minus:before { + content: "\e24f"; +} +.icon-document-1:before { + content: "\e250"; +} +.icon-document-2:before { + content: "\e251"; +} +.icon-file-1:before { + content: "\e252"; +} +.icon-file-2:before { + content: "\e253"; +} +.icon-file-add:before { + content: "\e254"; +} +.icon-file-attention:before { + content: "\e255"; +} +.icon-file-block:before { + content: "\e256"; +} +.icon-file-bookmark:before { + content: "\e257"; +} +.icon-file-checked:before { + content: "\e258"; +} +.icon-file-code:before { + content: "\e259"; +} +.icon-file-delete:before { + content: "\e25a"; +} +.icon-file-download:before { + content: "\e25b"; +} +.icon-file-edit:before { + content: "\e25c"; +} +.icon-file-favorite-1:before { + content: "\e25d"; +} +.icon-file-favorite-2:before { + content: "\e25e"; +} +.icon-file-graph-1:before { + content: "\e25f"; +} +.icon-file-graph-2:before { + content: "\e260"; +} +.icon-file-home:before { + content: "\e261"; +} +.icon-file-image-1:before { + content: "\e262"; +} +.icon-file-image-2:before { + content: "\e263"; +} +.icon-file-list:before { + content: "\e264"; +} +.icon-file-lock:before { + content: "\e265"; +} +.icon-file-media:before { + content: "\e266"; +} +.icon-file-minus:before { + content: "\e267"; +} +.icon-file-music:before { + content: "\e268"; +} +.icon-file-new:before { + content: "\e269"; +} +.icon-file-registry:before { + content: "\e26a"; +} +.icon-file-search:before { + content: "\e26b"; +} +.icon-file-setting:before { + content: "\e26c"; +} +.icon-file-sync:before { + content: "\e26d"; +} +.icon-file-table:before { + content: "\e26e"; +} +.icon-file-thumbnail:before { + content: "\e26f"; +} +.icon-file-time:before { + content: "\e270"; +} +.icon-file-transfer:before { + content: "\e271"; +} +.icon-file-upload:before { + content: "\e272"; +} +.icon-file-zip:before { + content: "\e273"; +} +.icon-files-1:before { + content: "\e274"; +} +.icon-files-2:before { + content: "\e275"; +} +.icon-files-3:before { + content: "\e276"; +} +.icon-files-4:before { + content: "\e277"; +} +.icon-files-5:before { + content: "\e278"; +} +.icon-files-6:before { + content: "\e279"; +} +.icon-hand-file-1:before { + content: "\e27a"; +} +.icon-hand-file-2:before { + content: "\e27b"; +} +.icon-note-paper-1:before { + content: "\e27c"; +} +.icon-note-paper-2:before { + content: "\e27d"; +} +.icon-note-paper-add:before { + content: "\e27e"; +} +.icon-note-paper-attention:before { + content: "\e27f"; +} +.icon-note-paper-block:before { + content: "\e280"; +} +.icon-note-paper-checked:before { + content: "\e281"; +} +.icon-note-paper-delete:before { + content: "\e282"; +} +.icon-note-paper-download:before { + content: "\e283"; +} +.icon-note-paper-edit:before { + content: "\e284"; +} +.icon-note-paper-favorite:before { + content: "\e285"; +} +.icon-note-paper-lock:before { + content: "\e286"; +} +.icon-note-paper-minus:before { + content: "\e287"; +} +.icon-note-paper-search:before { + content: "\e288"; +} +.icon-note-paper-sync:before { + content: "\e289"; +} +.icon-note-paper-upload:before { + content: "\e28a"; +} +.icon-print:before { + content: "\e28b"; +} +.icon-folder-1:before { + content: "\e28c"; +} +.icon-folder-2:before { + content: "\e28d"; +} +.icon-folder-3:before { + content: "\e28e"; +} +.icon-folder-4:before { + content: "\e28f"; +} +.icon-folder-add:before { + content: "\e290"; +} +.icon-folder-attention:before { + content: "\e291"; +} +.icon-folder-block:before { + content: "\e292"; +} +.icon-folder-bookmark:before { + content: "\e293"; +} +.icon-folder-checked:before { + content: "\e294"; +} +.icon-folder-code:before { + content: "\e295"; +} +.icon-folder-delete:before { + content: "\e296"; +} +.icon-folder-download:before { + content: "\e297"; +} +.icon-folder-edit:before { + content: "\e298"; +} +.icon-folder-favorite:before { + content: "\e299"; +} +.icon-folder-home:before { + content: "\e29a"; +} +.icon-folder-image:before { + content: "\e29b"; +} +.icon-folder-lock:before { + content: "\e29c"; +} +.icon-folder-media:before { + content: "\e29d"; +} +.icon-folder-minus:before { + content: "\e29e"; +} +.icon-folder-music:before { + content: "\e29f"; +} +.icon-folder-new:before { + content: "\e2a0"; +} +.icon-folder-search:before { + content: "\e2a1"; +} +.icon-folder-setting:before { + content: "\e2a2"; +} +.icon-folder-share-1:before { + content: "\e2a3"; +} +.icon-folder-share-2:before { + content: "\e2a4"; +} +.icon-folder-sync:before { + content: "\e2a5"; +} +.icon-folder-transfer:before { + content: "\e2a6"; +} +.icon-folder-upload:before { + content: "\e2a7"; +} +.icon-folder-zip:before { + content: "\e2a8"; +} +.icon-add-1:before { + content: "\e2a9"; +} +.icon-add-2:before { + content: "\e2aa"; +} +.icon-add-3:before { + content: "\e2ab"; +} +.icon-add-4:before { + content: "\e2ac"; +} +.icon-add-tag:before { + content: "\e2ad"; +} +.icon-arrow-1:before { + content: "\e2ae"; +} +.icon-arrow-2:before { + content: "\e2af"; +} +.icon-arrow-down-1:before { + content: "\e2b0"; +} +.icon-arrow-down-2:before { + content: "\e2b1"; +} +.icon-arrow-left-1:before { + content: "\e2b2"; +} +.icon-arrow-left-2:before { + content: "\e2b3"; +} +.icon-arrow-move-1:before { + content: "\e2b4"; +} +.icon-arrow-move-down:before { + content: "\e2b5"; +} +.icon-arrow-move-left:before { + content: "\e2b6"; +} +.icon-arrow-move-right:before { + content: "\e2b7"; +} +.icon-arrow-move-up:before { + content: "\e2b8"; +} +.icon-arrow-right-1:before { + content: "\e2b9"; +} +.icon-arrow-right-2:before { + content: "\e2ba"; +} +.icon-arrow-up-1:before { + content: "\e2bb"; +} +.icon-arrow-up-2:before { + content: "\e2bc"; +} +.icon-back:before { + content: "\e2bd"; +} +.icon-center-expand:before { + content: "\e2be"; +} +.icon-center-reduce:before { + content: "\e2bf"; +} +.icon-delete-1-1:before { + content: "\e2c0"; +} +.icon-delete-2-1:before { + content: "\e2c1"; +} +.icon-delete-3:before { + content: "\e2c2"; +} +.icon-delete-4:before { + content: "\e2c3"; +} +.icon-delete-tag:before { + content: "\e2c4"; +} +.icon-expand-horizontal:before { + content: "\e2c5"; +} +.icon-expand-vertical:before { + content: "\e2c6"; +} +.icon-forward-3:before { + content: "\e2c7"; +} +.icon-infinity:before { + content: "\e2c8"; +} +.icon-loading:before { + content: "\e2c9"; +} +.icon-log-out-1:before { + content: "\e2ca"; +} +.icon-loop-1:before { + content: "\e2cb"; +} +.icon-loop-2:before { + content: "\e2cc"; +} +.icon-loop-3:before { + content: "\e2cd"; +} +.icon-minus-1:before { + content: "\e2ce"; +} +.icon-minus-2:before { + content: "\e2cf"; +} +.icon-minus-3:before { + content: "\e2d0"; +} +.icon-minus-4:before { + content: "\e2d1"; +} +.icon-minus-tag:before { + content: "\e2d2"; +} +.icon-move-diagonal-1:before { + content: "\e2d3"; +} +.icon-move-diagonal-2:before { + content: "\e2d4"; +} +.icon-move-horizontal-1:before { + content: "\e2d5"; +} +.icon-move-horizontal-2:before { + content: "\e2d6"; +} +.icon-move-vertical-1:before { + content: "\e2d7"; +} +.icon-move-vertical-2:before { + content: "\e2d8"; +} +.icon-next-1-1:before { + content: "\e2d9"; +} +.icon-next-2-1:before { + content: "\e2da"; +} +.icon-power-1-1:before { + content: "\e2db"; +} +.icon-power-2-1:before { + content: "\e2dc"; +} +.icon-power-3:before { + content: "\e2dd"; +} +.icon-power-4:before { + content: "\e2de"; +} +.icon-power-5:before { + content: "\e2df"; +} +.icon-recycle:before { + content: "\e2e0"; +} +.icon-refresh:before { + content: "\e2e1"; +} +.icon-repeat:before { + content: "\e2e2"; +} +.icon-return:before { + content: "\e2e3"; +} +.icon-scale-all-1:before { + content: "\e2e4"; +} +.icon-scale-center:before { + content: "\e2e5"; +} +.icon-scale-horizontal-1:before { + content: "\e2e6"; +} +.icon-scale-horizontal-2:before { + content: "\e2e7"; +} +.icon-scale-reduce-1:before { + content: "\e2e8"; +} +.icon-scale-reduce-2:before { + content: "\e2e9"; +} +.icon-scale-reduce-3:before { + content: "\e2ea"; +} +.icon-scale-spread-1:before { + content: "\e2eb"; +} +.icon-scale-spread-2:before { + content: "\e2ec"; +} +.icon-scale-spread-3:before { + content: "\e2ed"; +} +.icon-scale-vertical-1:before { + content: "\e2ee"; +} +.icon-scale-vertical-2:before { + content: "\e2ef"; +} +.icon-scroll-horizontal-1:before { + content: "\e2f0"; +} +.icon-scroll-horizontal-2:before { + content: "\e2f1"; +} +.icon-scroll-omnidirectional-1:before { + content: "\e2f2"; +} +.icon-scroll-omnidirectional-2:before { + content: "\e2f3"; +} +.icon-scroll-vertical-1:before { + content: "\e2f4"; +} +.icon-scroll-vertical-2:before { + content: "\e2f5"; +} +.icon-shuffle:before { + content: "\e2f6"; +} +.icon-split:before { + content: "\e2f7"; +} +.icon-sync-1:before { + content: "\e2f8"; +} +.icon-sync-2:before { + content: "\e2f9"; +} +.icon-timer:before { + content: "\e2fa"; +} +.icon-transfer:before { + content: "\e2fb"; +} +.icon-transfer-1-1:before { + content: "\e2fc"; +} +.icon-chat-1-1:before { + content: "\e2fd"; +} +.icon-chat-2-1:before { + content: "\e2fe"; +} +.icon-check-1:before { + content: "\e2ff"; +} +.icon-check-2:before { + content: "\e300"; +} +.icon-check-3:before { + content: "\e301"; +} +.icon-check-4:before { + content: "\e302"; +} +.icon-check-bubble:before { + content: "\e303"; +} +.icon-check-list:before { + content: "\e304"; +} +.icon-check-shield:before { + content: "\e305"; +} +.icon-cross-1:before { + content: "\e306"; +} +.icon-cross-bubble:before { + content: "\e307"; +} +.icon-cross-shield:before { + content: "\e308"; +} +.icon-briefcase:before { + content: "\e309"; +} +.icon-brightness-high:before { + content: "\e30a"; +} +.icon-brightness-low:before { + content: "\e30b"; +} +.icon-hammer-1:before { + content: "\e30c"; +} +.icon-hammer-2:before { + content: "\e30d"; +} +.icon-pulse:before { + content: "\e30e"; +} +.icon-scale:before { + content: "\e30f"; +} +.icon-screw-driver:before { + content: "\e310"; +} +.icon-setting-adjustment:before { + content: "\e311"; +} +.icon-setting-gear:before { + content: "\e312"; +} +.icon-setting-gears-1:before { + content: "\e313"; +} +.icon-setting-gears-2:before { + content: "\e314"; +} +.icon-setting-wrenches:before { + content: "\e315"; +} +.icon-switch-1:before { + content: "\e316"; +} +.icon-switch-2:before { + content: "\e317"; +} +.icon-wrench:before { + content: "\e318"; +} +.icon-alarm-1:before { + content: "\e319"; +} +.icon-alarm-clock:before { + content: "\e31a"; +} +.icon-alarm-no:before { + content: "\e31b"; +} +.icon-alarm-snooze:before { + content: "\e31c"; +} +.icon-bell:before { + content: "\e31d"; +} +.icon-calendar-1:before { + content: "\e31e"; +} +.icon-calendar-2:before { + content: "\e31f"; +} +.icon-clock-1:before { + content: "\e320"; +} +.icon-clock-2:before { + content: "\e321"; +} +.icon-clock-3:before { + content: "\e322"; +} +.icon-hourglass-1:before { + content: "\e323"; +} +.icon-hourglass-2:before { + content: "\e324"; +} +.icon-timer-1:before { + content: "\e325"; +} +.icon-timer-3-quarter-1:before { + content: "\e326"; +} +.icon-timer-3-quarter-2:before { + content: "\e327"; +} +.icon-timer-full-1:before { + content: "\e328"; +} +.icon-timer-full-2:before { + content: "\e329"; +} +.icon-timer-half-1:before { + content: "\e32a"; +} +.icon-timer-half-2:before { + content: "\e32b"; +} +.icon-timer-half-3:before { + content: "\e32c"; +} +.icon-timer-half-4:before { + content: "\e32d"; +} +.icon-timer-quarter-1:before { + content: "\e32e"; +} +.icon-timer-quarter-2:before { + content: "\e32f"; +} +.icon-watch-1:before { + content: "\e330"; +} +.icon-watch-2:before { + content: "\e331"; +} +.icon-alert-1:before { + content: "\e332"; +} +.icon-alert-2:before { + content: "\e333"; +} +.icon-alert-3:before { + content: "\e334"; +} +.icon-information:before { + content: "\e335"; +} +.icon-nuclear-1:before { + content: "\e336"; +} +.icon-nuclear-2:before { + content: "\e337"; +} +.icon-question-mark:before { + content: "\e338"; +} +.icon-abacus:before { + content: "\e339"; +} +.icon-amex-card:before { + content: "\e33a"; +} +.icon-atm:before { + content: "\e33b"; +} +.icon-balance:before { + content: "\e33c"; +} +.icon-bank-1:before { + content: "\e33d"; +} +.icon-bank-2:before { + content: "\e33e"; +} +.icon-bank-note:before { + content: "\e33f"; +} +.icon-bank-notes-1:before { + content: "\e340"; +} +.icon-bank-notes-2:before { + content: "\e341"; +} +.icon-bitcoins:before { + content: "\e342"; +} +.icon-board-1:before { + content: "\e343"; +} +.icon-box-1:before { + content: "\e344"; +} +.icon-box-2:before { + content: "\e345"; +} +.icon-box-3:before { + content: "\e346"; +} +.icon-box-download:before { + content: "\e347"; +} +.icon-box-shipping:before { + content: "\e348"; +} +.icon-box-upload:before { + content: "\e349"; +} +.icon-business-chart-1:before { + content: "\e34a"; +} +.icon-business-chart-2:before { + content: "\e34b"; +} +.icon-calculator-1:before { + content: "\e34c"; +} +.icon-calculator-2:before { + content: "\e34d"; +} +.icon-calculator-3:before { + content: "\e34e"; +} +.icon-cash-register:before { + content: "\e34f"; +} +.icon-chart-board:before { + content: "\e350"; +} +.icon-chart-down:before { + content: "\e351"; +} +.icon-chart-up:before { + content: "\e352"; +} +.icon-check:before { + content: "\e353"; +} +.icon-coins-1:before { + content: "\e354"; +} +.icon-coins-2:before { + content: "\e355"; +} +.icon-court:before { + content: "\e356"; +} +.icon-credit-card:before { + content: "\e357"; +} +.icon-credit-card-lock:before { + content: "\e358"; +} +.icon-delivery:before { + content: "\e359"; +} +.icon-dollar-bag:before { + content: "\e35a"; +} +.icon-dollar-currency-1:before { + content: "\e35b"; +} +.icon-dollar-currency-2:before { + content: "\e35c"; +} +.icon-dollar-currency-3:before { + content: "\e35d"; +} +.icon-dollar-currency-4:before { + content: "\e35e"; +} +.icon-euro-bag:before { + content: "\e35f"; +} +.icon-euro-currency-1:before { + content: "\e360"; +} +.icon-euro-currency-2:before { + content: "\e361"; +} +.icon-euro-currency-3:before { + content: "\e362"; +} +.icon-euro-currency-4:before { + content: "\e363"; +} +.icon-forklift:before { + content: "\e364"; +} +.icon-hand-card:before { + content: "\e365"; +} +.icon-hand-coin:before { + content: "\e366"; +} +.icon-keynote:before { + content: "\e367"; +} +.icon-master-card:before { + content: "\e368"; +} +.icon-money:before { + content: "\e369"; +} +.icon-parking-meter:before { + content: "\e36a"; +} +.icon-percent-1:before { + content: "\e36b"; +} +.icon-percent-2:before { + content: "\e36c"; +} +.icon-percent-3:before { + content: "\e36d"; +} +.icon-percent-4:before { + content: "\e36e"; +} +.icon-percent-5:before { + content: "\e36f"; +} +.icon-percent-up:before { + content: "\e370"; +} +.icon-pie-chart-1:before { + content: "\e371"; +} +.icon-pie-chart-2:before { + content: "\e372"; +} +.icon-piggy-bank:before { + content: "\e373"; +} +.icon-pound-currency-1:before { + content: "\e374"; +} +.icon-pound-currency-2:before { + content: "\e375"; +} +.icon-pound-currency-3:before { + content: "\e376"; +} +.icon-pound-currency-4:before { + content: "\e377"; +} +.icon-safe-1:before { + content: "\e378"; +} +.icon-safe-2:before { + content: "\e379"; +} +.icon-shop:before { + content: "\e37a"; +} +.icon-sign:before { + content: "\e37b"; +} +.icon-trolley:before { + content: "\e37c"; +} +.icon-truck-1:before { + content: "\e37d"; +} +.icon-truck-2:before { + content: "\e37e"; +} +.icon-visa-card:before { + content: "\e37f"; +} +.icon-yen-currency-1:before { + content: "\e380"; +} +.icon-yen-currency-2:before { + content: "\e381"; +} +.icon-yen-currency-3:before { + content: "\e382"; +} +.icon-yen-currency-4:before { + content: "\e383"; +} +.icon-add-marker-1:before { + content: "\e384"; +} +.icon-add-marker-2:before { + content: "\e385"; +} +.icon-add-marker-3:before { + content: "\e386"; +} +.icon-add-marker-4:before { + content: "\e387"; +} +.icon-add-marker-5:before { + content: "\e388"; +} +.icon-compass-1:before { + content: "\e389"; +} +.icon-compass-2:before { + content: "\e38a"; +} +.icon-compass-3:before { + content: "\e38b"; +} +.icon-delete-marker-1:before { + content: "\e38c"; +} +.icon-delete-marker-2:before { + content: "\e38d"; +} +.icon-delete-marker-3:before { + content: "\e38e"; +} +.icon-delete-marker-4:before { + content: "\e38f"; +} +.icon-delete-marker-5:before { + content: "\e390"; +} +.icon-favorite-marker:before { + content: "\e391"; +} +.icon-favorite-marker-1:before { + content: "\e392"; +} +.icon-favorite-marker-2:before { + content: "\e393"; +} +.icon-favorite-marker-3:before { + content: "\e394"; +} +.icon-globe:before { + content: "\e395"; +} +.icon-location:before { + content: "\e396"; +} +.icon-map-1:before { + content: "\e397"; +} +.icon-map-location:before { + content: "\e398"; +} +.icon-map-marker-1:before { + content: "\e399"; +} +.icon-map-marker-2:before { + content: "\e39a"; +} +.icon-map-marker-3:before { + content: "\e39b"; +} +.icon-map-marker-4:before { + content: "\e39c"; +} +.icon-map-pin:before { + content: "\e39d"; +} +.icon-map-pin-marker:before { + content: "\e39e"; +} +.icon-marker-1:before { + content: "\e39f"; +} +.icon-marker-2:before { + content: "\e3a0"; +} +.icon-marker-3:before { + content: "\e3a1"; +} +.icon-marker-4:before { + content: "\e3a2"; +} +.icon-minus-marker-1:before { + content: "\e3a3"; +} +.icon-minus-marker-2:before { + content: "\e3a4"; +} +.icon-minus-marker-3:before { + content: "\e3a5"; +} +.icon-minus-marker-4:before { + content: "\e3a6"; +} +.icon-pin-1-1:before { + content: "\e3a7"; +} +.icon-pin-2-1:before { + content: "\e3a8"; +} +.icon-pin-location:before { + content: "\e3a9"; +} +.icon-anchor:before { + content: "\e3aa"; +} +.icon-bank:before { + content: "\e3ab"; +} +.icon-beach:before { + content: "\e3ac"; +} +.icon-boat:before { + content: "\e3ad"; +} +.icon-building-1:before { + content: "\e3ae"; +} +.icon-building-2:before { + content: "\e3af"; +} +.icon-building-3:before { + content: "\e3b0"; +} +.icon-buildings-1:before { + content: "\e3b1"; +} +.icon-buildings-2:before { + content: "\e3b2"; +} +.icon-buildings-3:before { + content: "\e3b3"; +} +.icon-buildings-4:before { + content: "\e3b4"; +} +.icon-castle:before { + content: "\e3b5"; +} +.icon-column:before { + content: "\e3b6"; +} +.icon-direction-sign:before { + content: "\e3b7"; +} +.icon-factory:before { + content: "\e3b8"; +} +.icon-fence:before { + content: "\e3b9"; +} +.icon-garage:before { + content: "\e3ba"; +} +.icon-globe-1:before { + content: "\e3bb"; +} +.icon-globe-2:before { + content: "\e3bc"; +} +.icon-house-1:before { + content: "\e3bd"; +} +.icon-house-2:before { + content: "\e3be"; +} +.icon-house-3:before { + content: "\e3bf"; +} +.icon-house-4:before { + content: "\e3c0"; +} +.icon-library:before { + content: "\e3c1"; +} +.icon-light-house:before { + content: "\e3c2"; +} +.icon-pine-tree:before { + content: "\e3c3"; +} +.icon-pisa:before { + content: "\e3c4"; +} +.icon-skyscraper:before { + content: "\e3c5"; +} +.icon-temple:before { + content: "\e3c6"; +} +.icon-treasure-map:before { + content: "\e3c7"; +} +.icon-tree:before { + content: "\e3c8"; +} +.icon-attention:before { + content: "\e3c9"; +} +.icon-bug-1:before { + content: "\e3ca"; +} +.icon-bug-2:before { + content: "\e3cb"; +} +.icon-css3:before { + content: "\e3cc"; +} +.icon-firewall:before { + content: "\e3cd"; +} +.icon-html5:before { + content: "\e3ce"; +} +.icon-plugin-1:before { + content: "\e3cf"; +} +.icon-plugin-2:before { + content: "\e3d0"; +} +.icon-script:before { + content: "\e3d1"; +} +.icon-new-window:before { + content: "\e3d2"; +} +.icon-window-1:before { + content: "\e3d3"; +} +.icon-window-2:before { + content: "\e3d4"; +} +.icon-window-3:before { + content: "\e3d5"; +} +.icon-window-add:before { + content: "\e3d6"; +} +.icon-window-alert:before { + content: "\e3d7"; +} +.icon-window-check:before { + content: "\e3d8"; +} +.icon-window-code-1:before { + content: "\e3d9"; +} +.icon-window-code-2:before { + content: "\e3da"; +} +.icon-window-code-3:before { + content: "\e3db"; +} +.icon-window-column:before { + content: "\e3dc"; +} +.icon-window-delete:before { + content: "\e3dd"; +} +.icon-window-denied:before { + content: "\e3de"; +} +.icon-window-download-1:before { + content: "\e3df"; +} +.icon-window-download-2:before { + content: "\e3e0"; +} +.icon-window-edit:before { + content: "\e3e1"; +} +.icon-window-favorite-1:before { + content: "\e3e2"; +} +.icon-window-favorite-2:before { + content: "\e3e3"; +} +.icon-window-graph-1:before { + content: "\e3e4"; +} +.icon-window-graph-2:before { + content: "\e3e5"; +} +.icon-window-hand:before { + content: "\e3e6"; +} +.icon-window-home:before { + content: "\e3e7"; +} +.icon-window-list-1:before { + content: "\e3e8"; +} +.icon-window-list-2:before { + content: "\e3e9"; +} +.icon-window-lock:before { + content: "\e3ea"; +} +.icon-window-minimize:before { + content: "\e3eb"; +} +.icon-window-minus:before { + content: "\e3ec"; +} +.icon-window-refresh:before { + content: "\e3ed"; +} +.icon-window-registry:before { + content: "\e3ee"; +} +.icon-window-search:before { + content: "\e3ef"; +} +.icon-window-selection-1:before { + content: "\e3f0"; +} +.icon-window-selection-2:before { + content: "\e3f1"; +} +.icon-window-setting:before { + content: "\e3f2"; +} +.icon-window-sync:before { + content: "\e3f3"; +} +.icon-window-thumbnail-1:before { + content: "\e3f4"; +} +.icon-window-thumbnail-2:before { + content: "\e3f5"; +} +.icon-window-time:before { + content: "\e3f6"; +} +.icon-window-upload-1:before { + content: "\e3f7"; +} +.icon-window-upload-2:before { + content: "\e3f8"; +} +.icon-database:before { + content: "\e3f9"; +} +.icon-database-alert:before { + content: "\e3fa"; +} +.icon-database-block:before { + content: "\e3fb"; +} +.icon-database-check:before { + content: "\e3fc"; +} +.icon-database-delete:before { + content: "\e3fd"; +} +.icon-database-download:before { + content: "\e3fe"; +} +.icon-database-editor:before { + content: "\e3ff"; +} +.icon-database-lock:before { + content: "\e400"; +} +.icon-database-minus:before { + content: "\e401"; +} +.icon-database-network:before { + content: "\e402"; +} +.icon-database-plus:before { + content: "\e403"; +} +.icon-database-refresh:before { + content: "\e404"; +} +.icon-database-search:before { + content: "\e405"; +} +.icon-database-setting:before { + content: "\e406"; +} +.icon-database-sync:before { + content: "\e407"; +} +.icon-database-time:before { + content: "\e408"; +} +.icon-database-upload:before { + content: "\e409"; +} +.icon-battery-charging:before { + content: "\e40a"; +} +.icon-battery-full:before { + content: "\e40b"; +} +.icon-battery-high:before { + content: "\e40c"; +} +.icon-battery-low:before { + content: "\e40d"; +} +.icon-battery-medium:before { + content: "\e40e"; +} +.icon-cd-1:before { + content: "\e40f"; +} +.icon-cd-2:before { + content: "\e410"; +} +.icon-chip:before { + content: "\e411"; +} +.icon-computer:before { + content: "\e412"; +} +.icon-disc:before { + content: "\e413"; +} +.icon-filter:before { + content: "\e414"; +} +.icon-floppy-disk:before { + content: "\e415"; +} +.icon-gameboy:before { + content: "\e416"; +} +.icon-harddisk-1:before { + content: "\e417"; +} +.icon-harddisk-2:before { + content: "\e418"; +} +.icon-imac:before { + content: "\e419"; +} +.icon-ipad-1:before { + content: "\e41a"; +} +.icon-ipad-2:before { + content: "\e41b"; +} +.icon-ipod:before { + content: "\e41c"; +} +.icon-joystick-1:before { + content: "\e41d"; +} +.icon-joystick-2:before { + content: "\e41e"; +} +.icon-joystick-3:before { + content: "\e41f"; +} +.icon-keyboard-1:before { + content: "\e420"; +} +.icon-keyboard-2:before { + content: "\e421"; +} +.icon-kindle-1:before { + content: "\e422"; +} +.icon-kindle-2:before { + content: "\e423"; +} +.icon-laptop-1:before { + content: "\e424"; +} +.icon-laptop-2:before { + content: "\e425"; +} +.icon-memory-card:before { + content: "\e426"; +} +.icon-mobile-phone:before { + content: "\e427"; +} +.icon-mouse-1:before { + content: "\e428"; +} +.icon-mouse-2:before { + content: "\e429"; +} +.icon-mp3player:before { + content: "\e42a"; +} +.icon-plug-1:before { + content: "\e42b"; +} +.icon-plug-2:before { + content: "\e42c"; +} +.icon-plug-slot:before { + content: "\e42d"; +} +.icon-printer:before { + content: "\e42e"; +} +.icon-projector:before { + content: "\e42f"; +} +.icon-remote:before { + content: "\e430"; +} +.icon-router:before { + content: "\e431"; +} +.icon-screen-1:before { + content: "\e432"; +} +.icon-screen-2:before { + content: "\e433"; +} +.icon-screen-3:before { + content: "\e434"; +} +.icon-screen-4:before { + content: "\e435"; +} +.icon-smartphone-1:before { + content: "\e436"; +} +.icon-television-1:before { + content: "\e437"; +} +.icon-typewriter-1:before { + content: "\e438"; +} +.icon-typewriter-2:before { + content: "\e439"; +} +.icon-usb-1:before { + content: "\e43a"; +} +.icon-usb-2:before { + content: "\e43b"; +} +.icon-webcam:before { + content: "\e43c"; +} +.icon-wireless-router-1:before { + content: "\e43d"; +} +.icon-wireless-router-2:before { + content: "\e43e"; +} +.icon-bluetooth:before { + content: "\e43f"; +} +.icon-ethernet:before { + content: "\e440"; +} +.icon-ethernet-slot:before { + content: "\e441"; +} +.icon-firewire-1:before { + content: "\e442"; +} +.icon-firewire-2:before { + content: "\e443"; +} +.icon-network-1:before { + content: "\e444"; +} +.icon-network-2:before { + content: "\e445"; +} +.icon-server-1:before { + content: "\e446"; +} +.icon-server-2:before { + content: "\e447"; +} +.icon-server-3:before { + content: "\e448"; +} +.icon-usb:before { + content: "\e449"; +} +.icon-wireless-signal:before { + content: "\e44a"; +} +.icon-book:before { + content: "\e44b"; +} +.icon-book-1:before { + content: "\e44c"; +} +.icon-book-2:before { + content: "\e44d"; +} +.icon-book-3:before { + content: "\e44e"; +} +.icon-book-4:before { + content: "\e44f"; +} +.icon-book-5:before { + content: "\e450"; +} +.icon-book-6:before { + content: "\e451"; +} +.icon-book-7:before { + content: "\e452"; +} +.icon-book-download-1:before { + content: "\e453"; +} +.icon-book-download-2:before { + content: "\e454"; +} +.icon-book-favorite-1:before { + content: "\e455"; +} +.icon-bookmark-1-1:before { + content: "\e456"; +} +.icon-bookmark-2-1:before { + content: "\e457"; +} +.icon-bookmark-3-1:before { + content: "\e458"; +} +.icon-bookmark-4-1:before { + content: "\e459"; +} +.icon-books-1:before { + content: "\e45a"; +} +.icon-books-2:before { + content: "\e45b"; +} +.icon-books-3:before { + content: "\e45c"; +} +.icon-briefcase-1:before { + content: "\e45d"; +} +.icon-contact-book-1:before { + content: "\e45e"; +} +.icon-contact-book-2:before { + content: "\e45f"; +} +.icon-contact-book-3:before { + content: "\e460"; +} +.icon-contact-book-4:before { + content: "\e461"; +} +.icon-copyright:before { + content: "\e462"; +} +.icon-creative-commons:before { + content: "\e463"; +} +.icon-cube:before { + content: "\e464"; +} +.icon-data-filter:before { + content: "\e465"; +} +.icon-document-box-1:before { + content: "\e466"; +} +.icon-document-box-2:before { + content: "\e467"; +} +.icon-document-box-3:before { + content: "\e468"; +} +.icon-drawer-1:before { + content: "\e469"; +} +.icon-drawer-2:before { + content: "\e46a"; +} +.icon-drawer-3:before { + content: "\e46b"; +} +.icon-envelope:before { + content: "\e46c"; +} +.icon-favortie-book-2:before { + content: "\e46d"; +} +.icon-file:before { + content: "\e46e"; +} +.icon-files:before { + content: "\e46f"; +} +.icon-filter-1:before { + content: "\e470"; +} +.icon-filter-2:before { + content: "\e471"; +} +.icon-layers-1:before { + content: "\e472"; +} +.icon-list-1:before { + content: "\e473"; +} +.icon-list-2:before { + content: "\e474"; +} +.icon-newspaper-1:before { + content: "\e475"; +} +.icon-newspaper-2:before { + content: "\e476"; +} +.icon-registry-1:before { + content: "\e477"; +} +.icon-registry-2:before { + content: "\e478"; +} +.icon-shield-1:before { + content: "\e479"; +} +.icon-shield-2:before { + content: "\e47a"; +} +.icon-shield-3:before { + content: "\e47b"; +} +.icon-sketchbook:before { + content: "\e47c"; +} +.icon-sound-book:before { + content: "\e47d"; +} +.icon-thumbnails-1:before { + content: "\e47e"; +} +.icon-thumbnails-2:before { + content: "\e47f"; +} +.icon-hierarchy-1:before { + content: "\e480"; +} +.icon-hierarchy-2:before { + content: "\e481"; +} +.icon-hierarchy-3:before { + content: "\e482"; +} +.icon-hierarchy-4:before { + content: "\e483"; +} +.icon-hierarchy-5:before { + content: "\e484"; +} +.icon-hierarchy-6:before { + content: "\e485"; +} +.icon-hierarchy-7:before { + content: "\e486"; +} +.icon-hierarchy-8:before { + content: "\e487"; +} +.icon-network-1-1:before { + content: "\e488"; +} +.icon-network-2-1:before { + content: "\e489"; +} +.icon-backpack:before { + content: "\e48a"; +} +.icon-balance-1:before { + content: "\e48b"; +} +.icon-bed:before { + content: "\e48c"; +} +.icon-bench:before { + content: "\e48d"; +} +.icon-bomb-1:before { + content: "\e48e"; +} +.icon-bricks:before { + content: "\e48f"; +} +.icon-bullets:before { + content: "\e490"; +} +.icon-buoy-ring:before { + content: "\e491"; +} +.icon-campfire:before { + content: "\e492"; +} +.icon-can:before { + content: "\e493"; +} +.icon-candle:before { + content: "\e494"; +} +.icon-canon:before { + content: "\e495"; +} +.icon-cctv-1:before { + content: "\e496"; +} +.icon-cctv-2:before { + content: "\e497"; +} +.icon-chair:before { + content: "\e498"; +} +.icon-chair-director:before { + content: "\e499"; +} +.icon-cigarette:before { + content: "\e49a"; +} +.icon-construction-sign:before { + content: "\e49b"; +} +.icon-diamond:before { + content: "\e49c"; +} +.icon-disabled:before { + content: "\e49d"; +} +.icon-door:before { + content: "\e49e"; +} +.icon-drawer:before { + content: "\e49f"; +} +.icon-driller:before { + content: "\e4a0"; +} +.icon-dumbbell:before { + content: "\e4a1"; +} +.icon-fire-extinguisher:before { + content: "\e4a2"; +} +.icon-flashlight:before { + content: "\e4a3"; +} +.icon-gas-station:before { + content: "\e4a4"; +} +.icon-gun:before { + content: "\e4a5"; +} +.icon-lamp-1:before { + content: "\e4a6"; +} +.icon-lamp-2:before { + content: "\e4a7"; +} +.icon-lamp-3:before { + content: "\e4a8"; +} +.icon-lamp-4:before { + content: "\e4a9"; +} +.icon-lightbulb-1:before { + content: "\e4aa"; +} +.icon-lightbulb-2:before { + content: "\e4ab"; +} +.icon-measuring-tape:before { + content: "\e4ac"; +} +.icon-mine-cart:before { + content: "\e4ad"; +} +.icon-missile:before { + content: "\e4ae"; +} +.icon-ring:before { + content: "\e4af"; +} +.icon-scale-1:before { + content: "\e4b0"; +} +.icon-shovel:before { + content: "\e4b1"; +} +.icon-smoke-no:before { + content: "\e4b2"; +} +.icon-sofa-1:before { + content: "\e4b3"; +} +.icon-sofa-2:before { + content: "\e4b4"; +} +.icon-sofa-3:before { + content: "\e4b5"; +} +.icon-target:before { + content: "\e4b6"; +} +.icon-torch:before { + content: "\e4b7"; +} +.icon-traffic-cone:before { + content: "\e4b8"; +} +.icon-traffic-light-1:before { + content: "\e4b9"; +} +.icon-traffic-light-2:before { + content: "\e4ba"; +} +.icon-treasure-1:before { + content: "\e4bb"; +} +.icon-treasure-2:before { + content: "\e4bc"; +} +.icon-trowel:before { + content: "\e4bd"; +} +.icon-watering-can:before { + content: "\e4be"; +} +.icon-weigh:before { + content: "\e4bf"; +} +.icon-academic-cap:before { + content: "\e4c0"; +} +.icon-baseball-helmet:before { + content: "\e4c1"; +} +.icon-beanie:before { + content: "\e4c2"; +} +.icon-bike-helmet:before { + content: "\e4c3"; +} +.icon-bow:before { + content: "\e4c4"; +} +.icon-cap:before { + content: "\e4c5"; +} +.icon-chaplin:before { + content: "\e4c6"; +} +.icon-chef-hat:before { + content: "\e4c7"; +} +.icon-cloth-hanger:before { + content: "\e4c8"; +} +.icon-fins:before { + content: "\e4c9"; +} +.icon-football-helmet:before { + content: "\e4ca"; +} +.icon-glasses:before { + content: "\e4cb"; +} +.icon-glasses-1:before { + content: "\e4cc"; +} +.icon-glasses-2:before { + content: "\e4cd"; +} +.icon-magician-hat:before { + content: "\e4ce"; +} +.icon-monocle-1:before { + content: "\e4cf"; +} +.icon-monocle-2:before { + content: "\e4d0"; +} +.icon-necktie:before { + content: "\e4d1"; +} +.icon-polo-shirt:before { + content: "\e4d2"; +} +.icon-safety-helmet:before { + content: "\e4d3"; +} +.icon-scuba-tank:before { + content: "\e4d4"; +} +.icon-shirt-1:before { + content: "\e4d5"; +} +.icon-shirt-2:before { + content: "\e4d6"; +} +.icon-sneakers:before { + content: "\e4d7"; +} +.icon-snorkel:before { + content: "\e4d8"; +} +.icon-sombrero:before { + content: "\e4d9"; +} +.icon-sunglasses:before { + content: "\e4da"; +} +.icon-tall-hat:before { + content: "\e4db"; +} +.icon-trousers:before { + content: "\e4dc"; +} +.icon-walking-stick:before { + content: "\e4dd"; +} +.icon-arrow-redo:before { + content: "\e4de"; +} +.icon-arrow-undo:before { + content: "\e4df"; +} +.icon-bold:before { + content: "\e4e0"; +} +.icon-columns:before { + content: "\e4e1"; +} +.icon-eraser:before { + content: "\e4e2"; +} +.icon-font-color:before { + content: "\e4e3"; +} +.icon-html:before { + content: "\e4e4"; +} +.icon-italic:before { + content: "\e4e5"; +} +.icon-list-1-1:before { + content: "\e4e6"; +} +.icon-list-2-1:before { + content: "\e4e7"; +} +.icon-list-3:before { + content: "\e4e8"; +} +.icon-list-4:before { + content: "\e4e9"; +} +.icon-paragraph:before { + content: "\e4ea"; +} +.icon-paste:before { + content: "\e4eb"; +} +.icon-print-preview:before { + content: "\e4ec"; +} +.icon-quote:before { + content: "\e4ed"; +} +.icon-strikethrough:before { + content: "\e4ee"; +} +.icon-text:before { + content: "\e4ef"; +} +.icon-text-wrapping-1:before { + content: "\e4f0"; +} +.icon-text-wrapping-2:before { + content: "\e4f1"; +} +.icon-text-wrapping-3:before { + content: "\e4f2"; +} +.icon-underline:before { + content: "\e4f3"; +} +.icon-align-center:before { + content: "\e4f4"; +} +.icon-align-left:before { + content: "\e4f5"; +} +.icon-align-right:before { + content: "\e4f6"; +} +.icon-all-caps:before { + content: "\e4f7"; +} +.icon-arrange-2-1:before { + content: "\e4f8"; +} +.icon-arrange-2-2:before { + content: "\e4f9"; +} +.icon-arrange-2-3:before { + content: "\e4fa"; +} +.icon-arrange-2-4:before { + content: "\e4fb"; +} +.icon-arrange-3-1:before { + content: "\e4fc"; +} +.icon-arrange-3-2:before { + content: "\e4fd"; +} +.icon-arrange-3-3:before { + content: "\e4fe"; +} +.icon-arrange-3-4:before { + content: "\e4ff"; +} +.icon-arrange-3-5:before { + content: "\e500"; +} +.icon-arrange-4-1:before { + content: "\e501"; +} +.icon-arrange-4-2:before { + content: "\e502"; +} +.icon-arrange-4-3:before { + content: "\e503"; +} +.icon-arrange-5:before { + content: "\e504"; +} +.icon-consolidate-all:before { + content: "\e505"; +} +.icon-decrease-indent-1:before { + content: "\e506"; +} +.icon-decrease-indent-2:before { + content: "\e507"; +} +.icon-horizontal-page:before { + content: "\e508"; +} +.icon-increase-indent-1:before { + content: "\e509"; +} +.icon-increase-indent-2:before { + content: "\e50a"; +} +.icon-justify:before { + content: "\e50b"; +} +.icon-leading-1:before { + content: "\e50c"; +} +.icon-leading-2:before { + content: "\e50d"; +} +.icon-left-indent:before { + content: "\e50e"; +} +.icon-right-indent:before { + content: "\e50f"; +} +.icon-small-caps:before { + content: "\e510"; +} +.icon-vertical-page:before { + content: "\e511"; +} +.icon-alt-mac:before { + content: "\e512"; +} +.icon-alt-windows:before { + content: "\e513"; +} +.icon-arrow-down:before { + content: "\e514"; +} +.icon-arrow-down-left:before { + content: "\e515"; +} +.icon-arrow-down-right:before { + content: "\e516"; +} +.icon-arrow-left:before { + content: "\e517"; +} +.icon-arrow-right:before { + content: "\e518"; +} +.icon-arrow-up:before { + content: "\e519"; +} +.icon-arrow-up-left:before { + content: "\e51a"; +} +.icon-arrow-up-right:before { + content: "\e51b"; +} +.icon-asterisk-1:before { + content: "\e51c"; +} +.icon-asterisk-2:before { + content: "\e51d"; +} +.icon-back-tab-1:before { + content: "\e51e"; +} +.icon-back-tab-2:before { + content: "\e51f"; +} +.icon-backward-delete:before { + content: "\e520"; +} +.icon-blank:before { + content: "\e521"; +} +.icon-eject:before { + content: "\e522"; +} +.icon-enter-1:before { + content: "\e523"; +} +.icon-enter-2:before { + content: "\e524"; +} +.icon-escape:before { + content: "\e525"; +} +.icon-page-down:before { + content: "\e526"; +} +.icon-page-up:before { + content: "\e527"; +} +.icon-return-1:before { + content: "\e528"; +} +.icon-shift:before { + content: "\e529"; +} +.icon-shift-2:before { + content: "\e52a"; +} +.icon-tab:before { + content: "\e52b"; +} +.icon-apple:before { + content: "\e52c"; +} +.icon-beer:before { + content: "\e52d"; +} +.icon-boil:before { + content: "\e52e"; +} +.icon-bottle-1:before { + content: "\e52f"; +} +.icon-bottle-2:before { + content: "\e530"; +} +.icon-bottle-3:before { + content: "\e531"; +} +.icon-bottle-4:before { + content: "\e532"; +} +.icon-bread:before { + content: "\e533"; +} +.icon-burger-1:before { + content: "\e534"; +} +.icon-burger-2:before { + content: "\e535"; +} +.icon-cake-1:before { + content: "\e536"; +} +.icon-cake-2:before { + content: "\e537"; +} +.icon-champagne:before { + content: "\e538"; +} +.icon-cheese:before { + content: "\e539"; +} +.icon-cocktail-1:before { + content: "\e53a"; +} +.icon-cocktail-2:before { + content: "\e53b"; +} +.icon-cocktail-3:before { + content: "\e53c"; +} +.icon-coffee-cup:before { + content: "\e53d"; +} +.icon-coffee-cup-1:before { + content: "\e53e"; +} +.icon-coffee-pot:before { + content: "\e53f"; +} +.icon-deep-fry:before { + content: "\e540"; +} +.icon-energy-drink:before { + content: "\e541"; +} +.icon-espresso-machine:before { + content: "\e542"; +} +.icon-food-dome:before { + content: "\e543"; +} +.icon-fork-and-knife:before { + content: "\e544"; +} +.icon-fork-and-spoon:before { + content: "\e545"; +} +.icon-grape:before { + content: "\e546"; +} +.icon-grater:before { + content: "\e547"; +} +.icon-grill:before { + content: "\e548"; +} +.icon-hot-drinks-glass:before { + content: "\e549"; +} +.icon-hotdog:before { + content: "\e54a"; +} +.icon-ice-cream-1:before { + content: "\e54b"; +} +.icon-ice-cream-2:before { + content: "\e54c"; +} +.icon-ice-cream-3:before { + content: "\e54d"; +} +.icon-ice-drinks-glass:before { + content: "\e54e"; +} +.icon-juicer:before { + content: "\e54f"; +} +.icon-kitchen-timer:before { + content: "\e550"; +} +.icon-milk:before { + content: "\e551"; +} +.icon-orange:before { + content: "\e552"; +} +.icon-oven:before { + content: "\e553"; +} +.icon-pan-fry:before { + content: "\e554"; +} +.icon-pepper-salt:before { + content: "\e555"; +} +.icon-pizza:before { + content: "\e556"; +} +.icon-pop-corn:before { + content: "\e557"; +} +.icon-serving:before { + content: "\e558"; +} +.icon-soda:before { + content: "\e559"; +} +.icon-soda-can-1:before { + content: "\e55a"; +} +.icon-soda-can-2:before { + content: "\e55b"; +} +.icon-steam:before { + content: "\e55c"; +} +.icon-tea-pot:before { + content: "\e55d"; +} +.icon-thermometer-high:before { + content: "\e55e"; +} +.icon-thermometer-low:before { + content: "\e55f"; +} +.icon-thermometer-medium:before { + content: "\e560"; +} +.icon-water:before { + content: "\e561"; +} +.icon-wine:before { + content: "\e562"; +} +.icon-ambulance:before { + content: "\e563"; +} +.icon-beaker-1:before { + content: "\e564"; +} +.icon-beaker-2:before { + content: "\e565"; +} +.icon-blood:before { + content: "\e566"; +} +.icon-drug:before { + content: "\e567"; +} +.icon-first-aid:before { + content: "\e568"; +} +.icon-hashish:before { + content: "\e569"; +} +.icon-heart-pulse:before { + content: "\e56a"; +} +.icon-hospital-1:before { + content: "\e56b"; +} +.icon-hospital-2:before { + content: "\e56c"; +} +.icon-hospital-sign-1:before { + content: "\e56d"; +} +.icon-hospital-sign-2:before { + content: "\e56e"; +} +.icon-hospital-sign-3:before { + content: "\e56f"; +} +.icon-medicine:before { + content: "\e570"; +} +.icon-microscope:before { + content: "\e571"; +} +.icon-mortar-and-pestle:before { + content: "\e572"; +} +.icon-plaster:before { + content: "\e573"; +} +.icon-pulse-graph-1:before { + content: "\e574"; +} +.icon-pulse-graph-2:before { + content: "\e575"; +} +.icon-pulse-graph-3:before { + content: "\e576"; +} +.icon-red-cross:before { + content: "\e577"; +} +.icon-stethoscope:before { + content: "\e578"; +} +.icon-syringe:before { + content: "\e579"; +} +.icon-yin-yang:before { + content: "\e57a"; +} +.icon-balloon:before { + content: "\e57b"; +} +.icon-briefcase-lock:before { + content: "\e57c"; +} +.icon-card:before { + content: "\e57d"; +} +.icon-cards-1:before { + content: "\e57e"; +} +.icon-cards-2:before { + content: "\e57f"; +} +.icon-curtain:before { + content: "\e580"; +} +.icon-dice-1:before { + content: "\e581"; +} +.icon-dice-2:before { + content: "\e582"; +} +.icon-pacman:before { + content: "\e583"; +} +.icon-pacman-ghost:before { + content: "\e584"; +} +.icon-sign-1:before { + content: "\e585"; +} +.icon-smiley-happy:before { + content: "\e586"; +} +.icon-smiley-sad:before { + content: "\e587"; +} +.icon-smileys:before { + content: "\e588"; +} +.icon-suitcase-1:before { + content: "\e589"; +} +.icon-suitcase-2:before { + content: "\e58a"; +} +.icon-tetris:before { + content: "\e58b"; +} +.icon-ticket-1:before { + content: "\e58c"; +} +.icon-ticket-2:before { + content: "\e58d"; +} +.icon-ticket-3:before { + content: "\e58e"; +} +.icon-virus:before { + content: "\e58f"; +} +.icon-cloud-1:before { + content: "\e590"; +} +.icon-cloud-lightning:before { + content: "\e591"; +} +.icon-clouds:before { + content: "\e592"; +} +.icon-first-quarter-half-moon:before { + content: "\e593"; +} +.icon-full-moon:before { + content: "\e594"; +} +.icon-hail:before { + content: "\e595"; +} +.icon-heavy-rain:before { + content: "\e596"; +} +.icon-moon-cloud:before { + content: "\e597"; +} +.icon-rain:before { + content: "\e598"; +} +.icon-rain-lightning:before { + content: "\e599"; +} +.icon-snow:before { + content: "\e59a"; +} +.icon-sun:before { + content: "\e59b"; +} +.icon-sun-cloud:before { + content: "\e59c"; +} +.icon-thermometer:before { + content: "\e59d"; +} +.icon-third-quarter-half-moon:before { + content: "\e59e"; +} +.icon-umbrella:before { + content: "\e59f"; +} +.icon-waning-crescent-moon:before { + content: "\e5a0"; +} +.icon-waning-gibbous-moon:before { + content: "\e5a1"; +} +.icon-waxing-crescent-moon:before { + content: "\e5a2"; +} +.icon-waxing-gibbous-moon:before { + content: "\e5a3"; +} +.icon-bicycle:before { + content: "\e5a4"; +} +.icon-bus-1:before { + content: "\e5a5"; +} +.icon-bus-2:before { + content: "\e5a6"; +} +.icon-car-1:before { + content: "\e5a7"; +} +.icon-car-2:before { + content: "\e5a8"; +} +.icon-car-3:before { + content: "\e5a9"; +} +.icon-car-4:before { + content: "\e5aa"; +} +.icon-helicopter:before { + content: "\e5ab"; +} +.icon-mountain-bike:before { + content: "\e5ac"; +} +.icon-pickup:before { + content: "\e5ad"; +} +.icon-plane-1:before { + content: "\e5ae"; +} +.icon-plane-2:before { + content: "\e5af"; +} +.icon-plane-landing:before { + content: "\e5b0"; +} +.icon-plane-takeoff:before { + content: "\e5b1"; +} +.icon-road:before { + content: "\e5b2"; +} +.icon-road-bike:before { + content: "\e5b3"; +} +.icon-rocket:before { + content: "\e5b4"; +} +.icon-scooter:before { + content: "\e5b5"; +} +.icon-ship:before { + content: "\e5b6"; +} +.icon-train:before { + content: "\e5b7"; +} +.icon-tram:before { + content: "\e5b8"; +} +.icon-cactus:before { + content: "\e5b9"; +} +.icon-clover:before { + content: "\e5ba"; +} +.icon-flower:before { + content: "\e5bb"; +} +.icon-hand-eco:before { + content: "\e5bc"; +} +.icon-hand-globe:before { + content: "\e5bd"; +} +.icon-leaf:before { + content: "\e5be"; +} +.icon-light-eco:before { + content: "\e5bf"; +} +.icon-potted-plant-1:before { + content: "\e5c0"; +} +.icon-potted-plant-2:before { + content: "\e5c1"; +} +.icon-2-fingers-down-swipe:before { + content: "\e5c2"; +} +.icon-2-fingers-horizontal-swipe:before { + content: "\e5c3"; +} +.icon-2-fingers-left-swipe:before { + content: "\e5c4"; +} +.icon-2-fingers-omnidirectional-swipe:before { + content: "\e5c5"; +} +.icon-2-fingers-right-swipe:before { + content: "\e5c6"; +} +.icon-2-fingers-tab-hold:before { + content: "\e5c7"; +} +.icon-2-fingers-tap:before { + content: "\e5c8"; +} +.icon-2-fingers-up-swipe:before { + content: "\e5c9"; +} +.icon-2-fingers-vertical-swipe:before { + content: "\e5ca"; +} +.icon-2finger-double-tap:before { + content: "\e5cb"; +} +.icon-double-tap:before { + content: "\e5cc"; +} +.icon-drag-down:before { + content: "\e5cd"; +} +.icon-drag-horizontal:before { + content: "\e5ce"; +} +.icon-drag-left:before { + content: "\e5cf"; +} +.icon-drag-right:before { + content: "\e5d0"; +} +.icon-drag-up:before { + content: "\e5d1"; +} +.icon-drag-vertical:before { + content: "\e5d2"; +} +.icon-filck-down:before { + content: "\e5d3"; +} +.icon-flick-up:before { + content: "\e5d4"; +} +.icon-horizontal-flick:before { + content: "\e5d5"; +} +.icon-left-flick:before { + content: "\e5d6"; +} +.icon-omnidirectional-drag:before { + content: "\e5d7"; +} +.icon-omnidirectional-flick:before { + content: "\e5d8"; +} +.icon-omnidirectional-swipe:before { + content: "\e5d9"; +} +.icon-pinch:before { + content: "\e5da"; +} +.icon-right-flick:before { + content: "\e5db"; +} +.icon-rotate-clockwise:before { + content: "\e5dc"; +} +.icon-rotate-counterclockwise:before { + content: "\e5dd"; +} +.icon-spread:before { + content: "\e5de"; +} +.icon-swipe-down:before { + content: "\e5df"; +} +.icon-swipe-horizontal:before { + content: "\e5e0"; +} +.icon-swipe-left:before { + content: "\e5e1"; +} +.icon-swipe-right:before { + content: "\e5e2"; +} +.icon-swipe-up:before { + content: "\e5e3"; +} +.icon-swipe-vertical:before { + content: "\e5e4"; +} +.icon-tap:before { + content: "\e5e5"; +} +.icon-tap-hold:before { + content: "\e5e6"; +} +.icon-vertical-flick:before { + content: "\e5e7"; +} +.icon-arrow-1-1:before { + content: "\e5e8"; +} +.icon-arrow-2-1:before { + content: "\e5e9"; +} +.icon-arrow-3:before { + content: "\e5ea"; +} +.icon-arrow-4:before { + content: "\e5eb"; +} +.icon-arrow-5:before { + content: "\e5ec"; +} +.icon-arrow-6:before { + content: "\e5ed"; +} +.icon-arrow-7:before { + content: "\e5ee"; +} +.icon-arrow-8:before { + content: "\e5ef"; +} +.icon-arrow-9:before { + content: "\e5f0"; +} +.icon-arrow-10:before { + content: "\e5f1"; +} +.icon-arrow-11:before { + content: "\e5f2"; +} +.icon-arrow-12:before { + content: "\e5f3"; +} +.icon-arrow-13:before { + content: "\e5f4"; +} +.icon-arrow-14:before { + content: "\e5f5"; +} +.icon-arrow-15:before { + content: "\e5f6"; +} +.icon-arrow-16:before { + content: "\e5f7"; +} +.icon-arrow-17:before { + content: "\e5f8"; +} +.icon-arrow-18:before { + content: "\e5f9"; +} +.icon-arrow-19:before { + content: "\e5fa"; +} +.icon-arrow-20:before { + content: "\e5fb"; +} +.icon-arrow-21:before { + content: "\e5fc"; +} +.icon-arrow-22:before { + content: "\e5fd"; +} +.icon-arrow-23:before { + content: "\e5fe"; +} +.icon-arrow-24:before { + content: "\e5ff"; +} +.icon-arrow-25:before { + content: "\e600"; +} +.icon-arrow-26:before { + content: "\e601"; +} +.icon-arrow-27:before { + content: "\e602"; +} +.icon-arrow-28:before { + content: "\e603"; +} +.icon-arrow-29:before { + content: "\e604"; +} +.icon-arrow-30:before { + content: "\e605"; +} +.icon-arrow-31:before { + content: "\e606"; +} +.icon-arrow-32:before { + content: "\e607"; +} +.icon-arrow-33:before { + content: "\e608"; +} +.icon-arrow-34:before { + content: "\e609"; +} +.icon-arrow-35:before { + content: "\e60a"; +} +.icon-arrow-36:before { + content: "\e60b"; +} +.icon-arrow-37:before { + content: "\e60c"; +} +.icon-arrow-38:before { + content: "\e60d"; +} +.icon-arrow-39:before { + content: "\e60e"; +} +.icon-arrow-40:before { + content: "\e60f"; +} +.icon-arrow-41:before { + content: "\e610"; +} +.icon-arrow-42:before { + content: "\e611"; +} +.icon-arrow-43:before { + content: "\e612"; +} +.icon-arrow-44:before { + content: "\e613"; +} +.icon-arrow-45:before { + content: "\e614"; +} +.icon-arrow-46:before { + content: "\e615"; +} +.icon-arrow-47:before { + content: "\e616"; +} +.icon-arrow-48:before { + content: "\e617"; +} +.icon-arrow-49:before { + content: "\e618"; +} +.icon-arrow-50:before { + content: "\e619"; +} +.icon-arrow-51:before { + content: "\e61a"; +} +.icon-arrow-52:before { + content: "\e61b"; +} +.icon-arrow-53:before { + content: "\e61c"; +} +.icon-arrow-54:before { + content: "\e61d"; +} +.icon-arrow-55:before { + content: "\e61e"; +} +.icon-arrow-56:before { + content: "\e61f"; +} +.icon-arrow-57:before { + content: "\e620"; +} +.icon-arrow-58:before { + content: "\e621"; +} +.icon-arrow-59:before { + content: "\e622"; +} +.icon-arrow-60:before { + content: "\e623"; +} +.icon-arrow-61:before { + content: "\e624"; +} +.icon-arrow-62:before { + content: "\e625"; +} +.icon-arrow-63:before { + content: "\e626"; +} +.icon-arrow-64:before { + content: "\e627"; +} +.icon-arrow-65:before { + content: "\e628"; +} +.icon-arrow-66:before { + content: "\e629"; +} +.icon-arrow-67:before { + content: "\e62a"; +} +.icon-arrow-68:before { + content: "\e62b"; +} +.icon-arrow-69:before { + content: "\e62c"; +} +.icon-arrow-70:before { + content: "\e62d"; +} +.icon-arrow-71:before { + content: "\e62e"; +} +.icon-arrow-72:before { + content: "\e62f"; +} +.icon-arrow-circle-1:before { + content: "\e630"; +} +.icon-arrow-circle-2:before { + content: "\e631"; +} +.icon-arrow-circle-3:before { + content: "\e632"; +} +.icon-arrow-circle-4:before { + content: "\e633"; +} +.icon-arrow-circle-5:before { + content: "\e634"; +} +.icon-arrow-circle-6:before { + content: "\e635"; +} +.icon-arrow-circle-7:before { + content: "\e636"; +} +.icon-arrow-circle-8:before { + content: "\e637"; +} +.icon-arrow-circle-9:before { + content: "\e638"; +} +.icon-arrow-circle-10:before { + content: "\e639"; +} +.icon-arrow-circle-11:before { + content: "\e63a"; +} +.icon-arrow-circle-12:before { + content: "\e63b"; +} +.icon-arrow-circle-13:before { + content: "\e63c"; +} +.icon-arrow-circle-14:before { + content: "\e63d"; +} +.icon-arrow-circle-15:before { + content: "\e63e"; +} +.icon-arrow-circle-16:before { + content: "\e63f"; +} +.icon-arrow-circle-17:before { + content: "\e640"; +} +.icon-arrow-circle-18:before { + content: "\e641"; +} +.icon-arrow-circle-19:before { + content: "\e642"; +} +.icon-arrow-circle-20:before { + content: "\e643"; +} +.icon-arrow-circle-21:before { + content: "\e644"; +} +.icon-arrow-circle-22:before { + content: "\e645"; +} +.icon-arrow-circle-23:before { + content: "\e646"; +} +.icon-arrow-circle-24:before { + content: "\e647"; +} +.icon-arrow-circle-25:before { + content: "\e648"; +} +.icon-arrow-circle-26:before { + content: "\e649"; +} +.icon-arrow-circle-27:before { + content: "\e64a"; +} +.icon-arrow-circle-28:before { + content: "\e64b"; +} +.icon-arrow-circle-29:before { + content: "\e64c"; +} +.icon-arrow-circle-30:before { + content: "\e64d"; +} +.icon-arrow-delete-1:before { + content: "\e64e"; +} +.icon-arrow-delete-2:before { + content: "\e64f"; +} +.icon-arrow-dot-1:before { + content: "\e650"; +} +.icon-arrow-dot-2:before { + content: "\e651"; +} +.icon-arrow-dot-3:before { + content: "\e652"; +} +.icon-arrow-dot-4:before { + content: "\e653"; +} +.icon-arrow-dot-5:before { + content: "\e654"; +} +.icon-arrow-dot-6:before { + content: "\e655"; +} +.icon-arrow-rectangle-1:before { + content: "\e656"; +} +.icon-arrow-rectangle-2:before { + content: "\e657"; +} +.icon-arrow-rectangle-3:before { + content: "\e658"; +} +.icon-arrow-rectangle-4:before { + content: "\e659"; +} +.icon-arrow-rectangle-5:before { + content: "\e65a"; +} +.icon-arrow-rectangle-6:before { + content: "\e65b"; +} +.icon-arrow-rectangle-7:before { + content: "\e65c"; +} +.icon-arrow-rectangle-8:before { + content: "\e65d"; +} +.icon-arrow-rectangle-9:before { + content: "\e65e"; +} +.icon-arrow-rectangle-10:before { + content: "\e65f"; +} +.icon-arrow-rectangle-11:before { + content: "\e660"; +} +.icon-arrow-rectangle-12:before { + content: "\e661"; +} +.icon-arrow-rectangle-13:before { + content: "\e662"; +} +.icon-arrow-rectangle-14:before { + content: "\e663"; +} +.icon-arrow-rectangle-15:before { + content: "\e664"; +} +.icon-arrow-rectangle-16:before { + content: "\e665"; +} +.icon-arrow-rectangle-17:before { + content: "\e666"; +} +.icon-arrow-rectangle-18:before { + content: "\e667"; +} +.icon-arrow-rectangle-19:before { + content: "\e668"; +} +.icon-arrow-rectangle-20:before { + content: "\e669"; +} diff --git a/app/assets/stylesheets/bootstrap-tour.css.scss b/app/assets/stylesheets/bootstrap-tour.scss similarity index 100% rename from app/assets/stylesheets/bootstrap-tour.css.scss rename to app/assets/stylesheets/bootstrap-tour.scss diff --git a/app/assets/stylesheets/campaigns/common.css.scss b/app/assets/stylesheets/campaigns/common.css.scss deleted file mode 100644 index accf7b7c1..000000000 --- a/app/assets/stylesheets/campaigns/common.css.scss +++ /dev/null @@ -1,9 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ - -body .campaign-banner { - margin: auto; - width: 980px; - max-height: 120px; - text-align: center; - overflow: hidden; -} \ No newline at end of file diff --git a/app/assets/stylesheets/campaigns/common.scss b/app/assets/stylesheets/campaigns/common.scss new file mode 100644 index 000000000..812f867de --- /dev/null +++ b/app/assets/stylesheets/campaigns/common.scss @@ -0,0 +1,10 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ + +body .campaign-banner { + margin: auto; + width: 980px; + max-height: 120px; + text-align: center; + overflow: hidden; + max-width: 100%; +} \ No newline at end of file diff --git a/app/assets/stylesheets/campaigns/edit/page.css.scss b/app/assets/stylesheets/campaigns/edit/page.scss similarity index 100% rename from app/assets/stylesheets/campaigns/edit/page.css.scss rename to app/assets/stylesheets/campaigns/edit/page.scss diff --git a/app/assets/stylesheets/campaigns/form.css.scss b/app/assets/stylesheets/campaigns/form.scss similarity index 100% rename from app/assets/stylesheets/campaigns/form.css.scss rename to app/assets/stylesheets/campaigns/form.scss diff --git a/app/assets/stylesheets/campaigns/index/page.css.scss b/app/assets/stylesheets/campaigns/index/page.scss similarity index 100% rename from app/assets/stylesheets/campaigns/index/page.css.scss rename to app/assets/stylesheets/campaigns/index/page.scss diff --git a/app/assets/stylesheets/campaigns/new/index.css.scss b/app/assets/stylesheets/campaigns/new/index.scss similarity index 100% rename from app/assets/stylesheets/campaigns/new/index.css.scss rename to app/assets/stylesheets/campaigns/new/index.scss diff --git a/app/assets/stylesheets/campaigns/peer_to_peer/page.css.scss b/app/assets/stylesheets/campaigns/peer_to_peer/page.scss similarity index 100% rename from app/assets/stylesheets/campaigns/peer_to_peer/page.css.scss rename to app/assets/stylesheets/campaigns/peer_to_peer/page.scss diff --git a/app/assets/stylesheets/campaigns/show/gift_levels.css.scss b/app/assets/stylesheets/campaigns/show/gift_levels.scss similarity index 100% rename from app/assets/stylesheets/campaigns/show/gift_levels.css.scss rename to app/assets/stylesheets/campaigns/show/gift_levels.scss diff --git a/app/assets/stylesheets/campaigns/show/page.css.scss b/app/assets/stylesheets/campaigns/show/page.scss similarity index 100% rename from app/assets/stylesheets/campaigns/show/page.css.scss rename to app/assets/stylesheets/campaigns/show/page.scss diff --git a/app/assets/stylesheets/campaigns/supporters/index/page.css.scss b/app/assets/stylesheets/campaigns/supporters/index/page.scss similarity index 100% rename from app/assets/stylesheets/campaigns/supporters/index/page.css.scss rename to app/assets/stylesheets/campaigns/supporters/index/page.scss diff --git a/app/assets/stylesheets/common/backgrounds.css.scss b/app/assets/stylesheets/common/backgrounds.css.scss deleted file mode 100644 index ca5fe82c0..000000000 --- a/app/assets/stylesheets/common/backgrounds.css.scss +++ /dev/null @@ -1,3 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -$paper: url('/assets/patterns/paper.png'); -$mosaic: url('/assets/patterns/color-mosaic.png'); diff --git a/app/assets/stylesheets/common/backgrounds.scss.erb b/app/assets/stylesheets/common/backgrounds.scss.erb new file mode 100644 index 000000000..f0a6b246a --- /dev/null +++ b/app/assets/stylesheets/common/backgrounds.scss.erb @@ -0,0 +1,4 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +$paper: url('<%= asset_path 'patterns/paper.png' %>'); +$mosaic: url('<%= asset_path 'patterns/color-mosaic.png' %>'); + diff --git a/app/assets/stylesheets/common/branded_campaign_button.css.scss b/app/assets/stylesheets/common/branded_campaign_button.scss similarity index 100% rename from app/assets/stylesheets/common/branded_campaign_button.css.scss rename to app/assets/stylesheets/common/branded_campaign_button.scss diff --git a/app/assets/stylesheets/common/campaign_card.css.scss b/app/assets/stylesheets/common/campaign_card.scss similarity index 100% rename from app/assets/stylesheets/common/campaign_card.css.scss rename to app/assets/stylesheets/common/campaign_card.scss diff --git a/app/assets/stylesheets/common/colors.css.scss b/app/assets/stylesheets/common/colors.scss similarity index 100% rename from app/assets/stylesheets/common/colors.css.scss rename to app/assets/stylesheets/common/colors.scss diff --git a/app/assets/stylesheets/common/donate_button.css.scss b/app/assets/stylesheets/common/donate_button.css.scss deleted file mode 100644 index f00f549ae..000000000 --- a/app/assets/stylesheets/common/donate_button.css.scss +++ /dev/null @@ -1,7 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'supporters/form'; -@import 'nonprofits/donation_form/title_row'; -@import 'nonprofits/donation_form/footer'; -@import 'nonprofits/donation_form/form'; // for styling the actual form -@import 'nonprofits/donation_form/show/index'; // for styling the layout on /donate diff --git a/app/assets/stylesheets/common/donate_button.scss b/app/assets/stylesheets/common/donate_button.scss new file mode 100644 index 000000000..e4b6bf533 --- /dev/null +++ b/app/assets/stylesheets/common/donate_button.scss @@ -0,0 +1,6 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'nonprofits/donation_form/title_row'; +@import 'nonprofits/donation_form/footer'; +@import 'nonprofits/donation_form/form'; // for styling the actual form +@import 'nonprofits/donation_form/show/index'; // for styling the layout on /donate diff --git a/app/assets/stylesheets/common/editable.css.scss.erb b/app/assets/stylesheets/common/editable.scss.erb similarity index 100% rename from app/assets/stylesheets/common/editable.css.scss.erb rename to app/assets/stylesheets/common/editable.scss.erb diff --git a/app/assets/stylesheets/common/fade_text.css.scss b/app/assets/stylesheets/common/fade_text.scss similarity index 100% rename from app/assets/stylesheets/common/fade_text.css.scss rename to app/assets/stylesheets/common/fade_text.scss diff --git a/app/assets/stylesheets/common/fonts_special.css.scss b/app/assets/stylesheets/common/fonts_special.scss similarity index 100% rename from app/assets/stylesheets/common/fonts_special.css.scss rename to app/assets/stylesheets/common/fonts_special.scss diff --git a/app/assets/stylesheets/common/fundraiser/metrics.css.scss b/app/assets/stylesheets/common/fundraiser/metrics.scss similarity index 100% rename from app/assets/stylesheets/common/fundraiser/metrics.css.scss rename to app/assets/stylesheets/common/fundraiser/metrics.scss diff --git a/app/assets/stylesheets/common/fundraisers.css.scss b/app/assets/stylesheets/common/fundraisers.scss similarity index 100% rename from app/assets/stylesheets/common/fundraisers.css.scss rename to app/assets/stylesheets/common/fundraisers.scss diff --git a/app/assets/stylesheets/common/icons.css.scss b/app/assets/stylesheets/common/icons.scss similarity index 100% rename from app/assets/stylesheets/common/icons.css.scss rename to app/assets/stylesheets/common/icons.scss diff --git a/app/assets/stylesheets/common/image_uploader.css.scss b/app/assets/stylesheets/common/image_uploader.scss similarity index 100% rename from app/assets/stylesheets/common/image_uploader.css.scss rename to app/assets/stylesheets/common/image_uploader.scss diff --git a/app/assets/stylesheets/common/images.css.scss b/app/assets/stylesheets/common/images.scss similarity index 100% rename from app/assets/stylesheets/common/images.css.scss rename to app/assets/stylesheets/common/images.scss diff --git a/app/assets/stylesheets/common/ios_hack.scss b/app/assets/stylesheets/common/ios_hack.scss deleted file mode 100644 index 7a73c7b10..000000000 --- a/app/assets/stylesheets/common/ios_hack.scss +++ /dev/null @@ -1,4 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -.ios-force-absolute-positioning { - position: absolute !important; -} \ No newline at end of file diff --git a/app/assets/stylesheets/common/layouts.css.scss b/app/assets/stylesheets/common/layouts.scss similarity index 100% rename from app/assets/stylesheets/common/layouts.css.scss rename to app/assets/stylesheets/common/layouts.scss diff --git a/app/assets/stylesheets/common/media_queries.css.scss b/app/assets/stylesheets/common/media_queries.scss similarity index 100% rename from app/assets/stylesheets/common/media_queries.css.scss rename to app/assets/stylesheets/common/media_queries.scss diff --git a/app/assets/stylesheets/common/minimal.css.scss b/app/assets/stylesheets/common/minimal.scss similarity index 100% rename from app/assets/stylesheets/common/minimal.css.scss rename to app/assets/stylesheets/common/minimal.scss diff --git a/app/assets/stylesheets/common/minimal_vertically_center.css.scss b/app/assets/stylesheets/common/minimal_vertically_center.scss similarity index 100% rename from app/assets/stylesheets/common/minimal_vertically_center.css.scss rename to app/assets/stylesheets/common/minimal_vertically_center.scss diff --git a/app/assets/stylesheets/common/page.css.scss b/app/assets/stylesheets/common/page.scss similarity index 100% rename from app/assets/stylesheets/common/page.css.scss rename to app/assets/stylesheets/common/page.scss diff --git a/app/assets/stylesheets/common/promote_button.css.scss b/app/assets/stylesheets/common/promote_button.scss similarity index 100% rename from app/assets/stylesheets/common/promote_button.css.scss rename to app/assets/stylesheets/common/promote_button.scss diff --git a/app/assets/stylesheets/common/states.css.scss b/app/assets/stylesheets/common/states.scss similarity index 100% rename from app/assets/stylesheets/common/states.css.scss rename to app/assets/stylesheets/common/states.scss diff --git a/app/assets/stylesheets/common/successes.css.scss b/app/assets/stylesheets/common/successes.scss similarity index 100% rename from app/assets/stylesheets/common/successes.css.scss rename to app/assets/stylesheets/common/successes.scss diff --git a/app/assets/stylesheets/common/typography/base.css.scss b/app/assets/stylesheets/common/typography/base.scss similarity index 100% rename from app/assets/stylesheets/common/typography/base.css.scss rename to app/assets/stylesheets/common/typography/base.scss diff --git a/app/assets/stylesheets/common/typography/special.css.scss b/app/assets/stylesheets/common/typography/special.scss similarity index 100% rename from app/assets/stylesheets/common/typography/special.css.scss rename to app/assets/stylesheets/common/typography/special.scss diff --git a/app/assets/stylesheets/common/utils.css.scss b/app/assets/stylesheets/common/utils.css.scss deleted file mode 100644 index 9b422a2d4..000000000 --- a/app/assets/stylesheets/common/utils.css.scss +++ /dev/null @@ -1,414 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -.hide {display: none; } -.show {display: block;} -.hide.show {display: block;} - -.u-inline, .hide.u-inline {display: inline;} -.u-block, .hide.u-block {display: block !important;} -.u-inlineBlock, .hide.u-inlineBlock {display: inline-block;} -.u-displayNone { display: none; } - -.u-hide {display: none !important;} - -.u-normal {font-weight: normal;} - -.u-strike { text-decoration: line-through;} -.u-flat { - margin: 0; - line-height: 1; -} - -.u-noClear { - clear: none; -} - -.u-smallPs p { - font-size: 14px; - line-height: 1.2; - margin: 0; -} - -.u-medPs p { - font-size: 15px; - line-height: 1.5; - margin: 0; -} - -.u-pointer { - cursor: pointer; -} - -.u-verticallyCenter { - display: table-cell; - vertical-align: middle; -} - -.u-verticallyCenterAbs { - position: absolute; - top: 50%; - transform: translateY(-50%); -} - -.u-centeredBg { - background-size: cover; - background-position: center; -} - -.u-overflow--hidden { - overflow: hidden !important; -} -.u-breakWord { - word-break: break-word; -} -.u-width--initial {width: initial; } -.u-width--full {width: 100%; } -.u-width--half {width: 50%; } -.u-width--third {width: 33.333333%; } -.u-width--40 {width: 40px; } -.u-width--50 {width: 50px; } -.u-width--60 {width: 60px; } -.u-width--70 {width: 70px; } -.u-width--80 {width: 80px; } -.u-width--90 {width: 90px; } -.u-width--100 {width: 100px; } -.u-width--140 {width: 140px; } -.u-width--150 {width: 150px; } -.u-width--160 {width: 160px; } -.u-width--200 {width: 200px; } -.u-width--250 {width: 250px; } -.u-width--300 {width: 300px; } -.u-width--400 {width: 400px; } - -.u-maxWidth--100 { max-width: 100px; } -.u-maxWidth--150 { max-width: 150px; } -.u-maxWidth--200 { max-width: 200px; } -.u-maxWidth--250 { max-width: 250px; } -.u-maxWidth--300 { max-width: 300px; } -.u-maxWidth--400 { max-width: 400px; } -.u-maxWidth--500 { max-width: 500px; } -.u-maxWidth--600 { max-width: 600px; } -.u-maxWidth--700 { max-width: 700px; } -.u-maxWidth--800 { max-width: 800px; } - -.u-height--80 { height: 80px; } -.u-height--100 { height: 100px; } -.u-height--200 { height: 200px; } -.u-height--full { height: 100%; } -.u-height--auto { height: auto; } - -.u-fontSize--12 {font-size: 12px !important; } -.u-fontSize--13 {font-size: 13px !important; } -.u-fontSize--14 {font-size: 14px !important; } -.u-fontSize--15 {font-size: 15px !important; } -.u-fontSize--16 {font-size: 16px !important; } -.u-fontSize--17 {font-size: 17px !important; } -.u-fontSize--18 {font-size: 18px !important; } -.u-fontSize--20 {font-size: 20px !important; } -.u-fontSize--22 {font-size: 22px !important; } -.u-fontSize--24 {font-size: 24px !important; } -.u-fontSize--26 {font-size: 26px !important; } -.u-fontSize--28 {font-size: 28px !important; } -.u-fontSize--40 {font-size: 40px !important; } -.u-fontSize--50 {font-size: 50px !important; } - -.u-margin--auto { margin: auto !important; } -.u-margin--0 { margin: 0 !important; } -.u-margin--5 { margin: 5px !important; } -.u-margin--10 { margin: 10px !important; } -.u-margin--20 { margin: 20px !important; } - -.u-marginBottom--0 { margin-bottom: 0 !important; } -.u-marginBottom--3 { margin-bottom: 3px !important; } -.u-marginBottom--5 { margin-bottom: 5px !important; } -.u-marginBottom--15 { margin-bottom: 15px !important; } -.u-marginBottom--10 { margin-bottom: 10px !important; } -.u-marginBottom--20 { margin-bottom: 20px !important; } -.u-marginBottom--25 { margin-bottom: 25px !important; } -.u-marginBottom--30 { margin-bottom: 30px !important; } -.u-marginBottom--40 { margin-bottom: 40px !important; } -.u-marginBottom--50 { margin-bottom: 50px !important; } -.u-marginBottom--60 { margin-bottom: 60px !important; } - -.u-marginTop--0 { margin-top: 0 !important; } -.u-marginTop--3 { margin-top: 3px !important; } -.u-marginTop--5 { margin-top: 5px !important; } -.u-marginTop--10 { margin-top: 10px !important; } -.u-marginTop--15 { margin-top: 15px !important; } -.u-marginTop--20 { margin-top: 20px !important; } -.u-marginTop--30 { margin-top: 30px !important; } -.u-marginTop--50 { margin-top: 50px !important; } -.u-marginTop--60 { margin-top: 60px !important; } - -.u-marginY--5 { margin-top: 5px; margin-bottom: 5px; } -.u-marginY--10 { margin-top: 10px; margin-bottom: 10px; } -.u-marginY--15 { margin-top: 15px; margin-bottom: 15px; } -.u-marginY--20 { margin-top: 20px; margin-bottom: 20px; } -.u-marginY--25 { margin-top: 25px; margin-bottom: 25px; } -.u-marginY--30 { margin-top: 30px; margin-bottom: 30px; } -.u-marginY--35 { margin-top: 35px; margin-bottom: 35px; } -.u-marginY--40 { margin-top: 40px; margin-bottom: 40px; } -.u-marginY--50 { margin-top: 50px; margin-bottom: 50px; } -.u-marginY--60 { margin-top: 60px; margin-bottom: 60px; } - -.u-flatHeight { - margin: 0 !important; - line-height: 1 !important; -} - -.u-marginX--10 { margin: 0 10px; } -.u-marginX--20 { margin: 0 20px; } -.u-marginX--30 { margin: 0 30px; } -.u-marginX--40 { margin: 0 40px; } -.u-marginX--50 { margin: 0 50px; } - - -.u-marginLeft--0 { margin-left: 0 !important; } -.u-marginLeft--5 { margin-left: 5px !important; } -.u-marginLeft--10 { margin-left: 10px !important; } -.u-marginLeft--20 { margin-left: 20px !important; } - -.u-marginRight--0 { margin-right: 0 !important; } -.u-marginRight--5 { margin-right: 5px !important; } -.u-marginRight--10 { margin-right: 10px !important; } -.u-marginRight--20 { margin-right: 20px !important; } - -.padded { padding: 20px; } - -.u-paddingTop--0 { padding-top: 0 !important; } -.u-paddingTop--5 { padding-top: 5px !important; } -.u-paddingTop--10 { padding-top: 10px !important; } -.u-paddingTop--15 { padding-top: 15px !important; } -.u-paddingTop--20 { padding-top: 20px !important; } -.u-paddingTop--30 { padding-top: 30px !important; } -.u-paddingTop--40 { padding-top: 40px !important; } - -.u-paddingBottom--0 { padding-bottom: 0 !important; } -.u-paddingBottom--10 { padding-bottom: 10px !important; } -.u-paddingBottom--15 { padding-bottom: 15px !important; } -.u-paddingBottom--20 { padding-bottom: 20px !important; } -.u-paddingBottom--30 { padding-bottom: 30px !important; } - -.u-padding--0 { padding: 0 !important; } -.u-padding--3 { padding: 3px !important; } -.u-padding--5 { padding: 5px !important; } -.u-padding--8 { padding: 8px !important; } -.u-padding--10 { padding: 10px !important; } -.u-padding--15 { padding: 15px !important; } -.u-padding--20 { padding: 20px !important;} -.u-padding--25 { padding: 25px !important; } -.u-padding--30 { padding: 30px !important; } - - -.u-paddingX--0 { - padding-left: 0 !important; - padding-right: 0 !important; -} -.u-paddingX--5 { padding-left: 5px !important; padding-right: 5px !important; } -.u-paddingX--10 { padding: 0 10px !important; } -.u-paddingX--15 { padding: 0 15px !important; } -.u-paddingX--20 { padding: 0 20px !important;} -.u-paddingX--30 { padding: 0 30px !important;} -.u-paddingX--40 { padding: 0 40px !important;} - -.u-paddingY--10 { padding-top: 10px !important; padding-bottom: 10px !important; } -.u-paddingY--15 { padding: 15px 0 !important; } -.u-paddingY--20 { padding: 20px 0 !important;} -.u-paddingY--30 { padding: 30px 0 !important;} -.u-paddingY--40 { padding: 40px 0 !important;} - - -.u-paddingLeft--0 { padding-left: 0 !important; } -.u-paddingLeft--5 { padding-left: 10px; } -.u-paddingLeft--10 { padding-left: 10px; } -.u-paddingLeft--15 { padding-left: 15px; } -.u-paddingLeft--20 { padding-left: 20px; } -.u-paddingLeft--30 { padding-left: 30px; } -.u-paddingLeft--40 { padding-left: 40px; } - -.u-paddingRight--0 { padding-right: 0; } -.u-paddingRight--10 { padding-right: 10px; } -.u-paddingRight--15 { padding-right: 15px; } -.u-paddingRight--20 { padding-right: 20px; } -.u-paddingRight--30 { padding-right: 30px; } - -.u-top--0 { top: 0 !important; } - -.u-overflow--auto { overflow: auto;} -.u-overflow--hidden { overflow: hidden;} - -.u-textAlign--left { text-align: left; } -.u-textAlign--right { text-align: right; } - -.u-background--paper { background: $paper; } -.u-background--fog { background: $fog; } -.u-background--grey { background: rgba($grey, 0.07); } -.u-background--grey--dark { background: rgba($grey, 0.09); } -.u-background--white {background: white } - - -.u-capitalize {text-transform: capitalize; } - -.floatl, .u-floatL {float: left !important;} -.floatr, .u-floatR {float: right !important;} -.u-float--none { float: none !important; } - -.u-clear--none { - clear: none; -} - - -.u-color--charcoal { color: $charcoal; } -.u-color--grey { color: $grey; } -.u-color--lightGrey { color: $shark; } -.u-color--white { color: white; } -.u-color--red {color: $red;} -.u-color--green {color: $grass;} -.u-color--seaFoam {color: #669092 !important;} - -.u-invisible { - visibility: hidden; -} - -.u-bg--white { - background: white; -} -.u-bg--cloud { - background: rgba(white, 0.7); -} -.u-bg--fog { - background: $fog; -} - -.u-bg-blue-light { - background: rgba(66, 179, 223, 0.1); -} - -.u-dashedBorder { - border-bottom: 1px dashed $turquoise; -} - -.u-circle { - @include border-radius(50%); -} - -.u-border--light { - border: 2px solid $fog; -} - -.u-border--edit { - border-top: 1px solid #d9d9d9; - border-right: 1px solid #d9d9d9; - border-left: 1px solid #d9d9d9; - border-bottom: 2px solid #d9d9d9; -} - -.u-border--bottom { - border-bottom: 1px solid rgba(black, 0.05); -} - -.u-underline { - color: inherit; - text-decoration: underline; - &:hover { - color: inherit; - @include opacity(0.8); - } -} - -.no-border {border: 0;} -.clear {clear: both;} -.clearl {clear: left;} -.clearr {clear: right;} - -.centered, .u-centered { - text-align: center !important; -} -.u-centered--shallow { - text-align: center; - & > * { - text-align: left; - } -} - -.u-noSelect { - -moz-user-select: none; - -webkit-user-select: none; - -ms-user-select:none; - user-select:none; - -o-user-select:none; -} - - -.u-lineHeight--1 { - line-height: 1; -} - -.u-halfOpacity { - @include opacity(0.5); -} -.u-noOpacity { - @include opacity(0); -} -.u-fade { - @include opacity(0.7); -} -.u-bold {font-weight: bold !important; } - -.u-small {font-size: 14px; } - -.u-ellipses { - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.u-noTransition { - @include transition(none); -} - -.u-relative { - position: relative; -} - -.u-prepend { - position: relative; - &:before { - position: absolute; - color: $grey; - } -} - -.u-shadow { - @include box-shadow(0 1px 5px 0 rgba(0, 0, 0, 0.1)); -} - -.u-capitalize { - text-transform: capitalize; -} -.u-bitter { - font-family: 'Bitter'; -} -.u-strong { - font-weight: bold; -} - -.u-vAlign--bottom { - vertical-align: bottom; -} - -.u-fixed--bottom { - position: fixed; - bottom: 0; -} - -@media screen and (max-width: 600px) { - .u-hideIf--600 {display: none;} -} -@media screen and (max-width: 500px) { - .u-hideIf--500 {display: none;} -} -@media screen and (max-width: 400px) { - .u-hideIf--400 {display: none;} -} diff --git a/app/assets/stylesheets/common/utils.scss b/app/assets/stylesheets/common/utils.scss new file mode 100644 index 000000000..ae4b2df43 --- /dev/null +++ b/app/assets/stylesheets/common/utils.scss @@ -0,0 +1,423 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +.hide {display: none; } +.show {display: block;} +.hide.show {display: block;} + +.u-inline, .hide.u-inline {display: inline;} +.u-block, .hide.u-block {display: block !important;} +.u-inlineBlock, .hide.u-inlineBlock {display: inline-block;} +.u-displayNone { display: none; } + +.u-hide {display: none !important;} + +.u-normal {font-weight: normal;} + +.u-strike { text-decoration: line-through;} +.u-flat { + margin: 0; + line-height: 1; +} + +.u-noClear { + clear: none; +} + +.u-smallPs p { + font-size: 14px; + line-height: 1.2; + margin: 0; +} + +.u-medPs p { + font-size: 15px; + line-height: 1.5; + margin: 0; +} + +.u-pointer { + cursor: pointer; +} + +.u-verticallyCenter { + display: table-cell; + vertical-align: middle; +} + +.u-verticallyCenterAbs { + position: absolute; + top: 50%; + transform: translateY(-50%); +} + +.u-centeredBg { + background-size: cover; + background-position: center; +} + +.u-overflow--hidden { + overflow: hidden !important; +} +.u-breakWord { + word-break: break-word; +} +.u-width--initial {width: initial; } +.u-width--full {width: 100%; } +.u-width--half {width: 50%; } +.u-width--third {width: 33.333333%; } +.u-width--40 {width: 40px; } +.u-width--50 {width: 50px; } +.u-width--60 {width: 60px; } +.u-width--70 {width: 70px; } +.u-width--80 {width: 80px; } +.u-width--90 {width: 90px; } +.u-width--100 {width: 100px; } +.u-width--140 {width: 140px; } +.u-width--150 {width: 150px; } +.u-width--160 {width: 160px; } +.u-width--200 {width: 200px; } +.u-width--250 {width: 250px; } +.u-width--300 {width: 300px; } +.u-width--400 {width: 400px; } + +.u-maxWidth--100 { max-width: 100px; } +.u-maxWidth--150 { max-width: 150px; } +.u-maxWidth--200 { max-width: 200px; } +.u-maxWidth--250 { max-width: 250px; } +.u-maxWidth--300 { max-width: 300px; } +.u-maxWidth--400 { max-width: 400px; } +.u-maxWidth--500 { max-width: 500px; } +.u-maxWidth--600 { max-width: 600px; } +.u-maxWidth--700 { max-width: 700px; } +.u-maxWidth--800 { max-width: 800px; } + +.u-height--80 { height: 80px; } +.u-height--100 { height: 100px; } +.u-height--200 { height: 200px; } +.u-height--full { height: 100%; } +.u-height--auto { height: auto; } + +.u-fontSize--12 {font-size: 12px !important; } +.u-fontSize--13 {font-size: 13px !important; } +.u-fontSize--14 {font-size: 14px !important; } +.u-fontSize--15 {font-size: 15px !important; } +.u-fontSize--16 {font-size: 16px !important; } +.u-fontSize--17 {font-size: 17px !important; } +.u-fontSize--18 {font-size: 18px !important; } +.u-fontSize--20 {font-size: 20px !important; } +.u-fontSize--22 {font-size: 22px !important; } +.u-fontSize--24 {font-size: 24px !important; } +.u-fontSize--26 {font-size: 26px !important; } +.u-fontSize--28 {font-size: 28px !important; } +.u-fontSize--40 {font-size: 40px !important; } +.u-fontSize--50 {font-size: 50px !important; } + +.u-margin--auto { margin: auto !important; } +.u-margin--0 { margin: 0 !important; } +.u-margin--5 { margin: 5px !important; } +.u-margin--10 { margin: 10px !important; } +.u-margin--20 { margin: 20px !important; } + +.u-marginBottom--0 { margin-bottom: 0 !important; } +.u-marginBottom--3 { margin-bottom: 3px !important; } +.u-marginBottom--5 { margin-bottom: 5px !important; } +.u-marginBottom--15 { margin-bottom: 15px !important; } +.u-marginBottom--10 { margin-bottom: 10px !important; } +.u-marginBottom--20 { margin-bottom: 20px !important; } +.u-marginBottom--25 { margin-bottom: 25px !important; } +.u-marginBottom--30 { margin-bottom: 30px !important; } +.u-marginBottom--40 { margin-bottom: 40px !important; } +.u-marginBottom--50 { margin-bottom: 50px !important; } +.u-marginBottom--60 { margin-bottom: 60px !important; } + +.u-marginTop--0 { margin-top: 0 !important; } +.u-marginTop--3 { margin-top: 3px !important; } +.u-marginTop--5 { margin-top: 5px !important; } +.u-marginTop--10 { margin-top: 10px !important; } +.u-marginTop--15 { margin-top: 15px !important; } +.u-marginTop--20 { margin-top: 20px !important; } +.u-marginTop--30 { margin-top: 30px !important; } +.u-marginTop--50 { margin-top: 50px !important; } +.u-marginTop--60 { margin-top: 60px !important; } + +.u-marginY--5 { margin-top: 5px; margin-bottom: 5px; } +.u-marginY--10 { margin-top: 10px; margin-bottom: 10px; } +.u-marginY--15 { margin-top: 15px; margin-bottom: 15px; } +.u-marginY--20 { margin-top: 20px; margin-bottom: 20px; } +.u-marginY--25 { margin-top: 25px; margin-bottom: 25px; } +.u-marginY--30 { margin-top: 30px; margin-bottom: 30px; } +.u-marginY--35 { margin-top: 35px; margin-bottom: 35px; } +.u-marginY--40 { margin-top: 40px; margin-bottom: 40px; } +.u-marginY--50 { margin-top: 50px; margin-bottom: 50px; } +.u-marginY--60 { margin-top: 60px; margin-bottom: 60px; } + +.u-flatHeight { + margin: 0 !important; + line-height: 1 !important; +} + +.u-marginX--10 { margin: 0 10px; } +.u-marginX--20 { margin: 0 20px; } +.u-marginX--30 { margin: 0 30px; } +.u-marginX--40 { margin: 0 40px; } +.u-marginX--50 { margin: 0 50px; } + + +.u-marginLeft--0 { margin-left: 0 !important; } +.u-marginLeft--5 { margin-left: 5px !important; } +.u-marginLeft--10 { margin-left: 10px !important; } +.u-marginLeft--20 { margin-left: 20px !important; } + +.u-marginRight--0 { margin-right: 0 !important; } +.u-marginRight--5 { margin-right: 5px !important; } +.u-marginRight--10 { margin-right: 10px !important; } +.u-marginRight--20 { margin-right: 20px !important; } + +.padded { padding: 20px; } + +.u-paddingTop--0 { padding-top: 0 !important; } +.u-paddingTop--5 { padding-top: 5px !important; } +.u-paddingTop--10 { padding-top: 10px !important; } +.u-paddingTop--15 { padding-top: 15px !important; } +.u-paddingTop--20 { padding-top: 20px !important; } +.u-paddingTop--30 { padding-top: 30px !important; } +.u-paddingTop--40 { padding-top: 40px !important; } + +.u-paddingBottom--0 { padding-bottom: 0 !important; } +.u-paddingBottom--10 { padding-bottom: 10px !important; } +.u-paddingBottom--15 { padding-bottom: 15px !important; } +.u-paddingBottom--20 { padding-bottom: 20px !important; } +.u-paddingBottom--30 { padding-bottom: 30px !important; } + +.u-padding--0 { padding: 0 !important; } +.u-padding--3 { padding: 3px !important; } +.u-padding--5 { padding: 5px !important; } +.u-padding--8 { padding: 8px !important; } +.u-padding--10 { padding: 10px !important; } +.u-padding--15 { padding: 15px !important; } +.u-padding--20 { padding: 20px !important;} +.u-padding--25 { padding: 25px !important; } +.u-padding--30 { padding: 30px !important; } + + +.u-paddingX--0 { + padding-left: 0 !important; + padding-right: 0 !important; +} +.u-paddingX--5 { padding-left: 5px !important; padding-right: 5px !important; } +.u-paddingX--10 { padding: 0 10px !important; } +.u-paddingX--15 { padding: 0 15px !important; } +.u-paddingX--20 { padding: 0 20px !important;} +.u-paddingX--30 { padding: 0 30px !important;} +.u-paddingX--40 { padding: 0 40px !important;} + +.u-paddingY--10 { padding-top: 10px !important; padding-bottom: 10px !important; } +.u-paddingY--15 { padding: 15px 0 !important; } +.u-paddingY--20 { padding: 20px 0 !important;} +.u-paddingY--30 { padding: 30px 0 !important;} +.u-paddingY--40 { padding: 40px 0 !important;} + + +.u-paddingLeft--0 { padding-left: 0 !important; } +.u-paddingLeft--5 { padding-left: 10px; } +.u-paddingLeft--10 { padding-left: 10px; } +.u-paddingLeft--15 { padding-left: 15px; } +.u-paddingLeft--20 { padding-left: 20px; } +.u-paddingLeft--30 { padding-left: 30px; } +.u-paddingLeft--40 { padding-left: 40px; } + +.u-paddingRight--0 { padding-right: 0; } +.u-paddingRight--10 { padding-right: 10px; } +.u-paddingRight--15 { padding-right: 15px; } +.u-paddingRight--20 { padding-right: 20px; } +.u-paddingRight--30 { padding-right: 30px; } + +.u-top--0 { top: 0 !important; } + +.u-overflow--auto { overflow: auto;} +.u-overflow--hidden { overflow: hidden;} + +.u-textAlign--left { text-align: left; } +.u-textAlign--right { text-align: right; } + +.u-background--paper { background: $paper; } +.u-background--fog { background: $fog; } +.u-background--grey { background: rgba($grey, 0.07); } +.u-background--grey--dark { background: rgba($grey, 0.09); } +.u-background--white {background: white } + + +.u-capitalize {text-transform: capitalize; } + +.floatl, .u-floatL {float: left !important;} +.floatr, .u-floatR {float: right !important;} +.u-float--none { float: none !important; } + +.u-clear--none { + clear: none; +} + + +.u-color--charcoal { color: $charcoal; } +.u-color--grey { color: $grey; } +.u-color--lightGrey { color: $shark; } +.u-color--white { color: white; } +.u-color--red {color: $red;} +.u-color--green {color: $grass;} +.u-color--seaFoam {color: #669092 !important;} + +.u-invisible { + visibility: hidden; +} + +.u-bg--white { + background: white; +} +.u-bg--cloud { + background: rgba(white, 0.7); +} +.u-bg--fog { + background: $fog; +} + +.u-bg-blue-light { + background: rgba(66, 179, 223, 0.1); +} + +.u-dashedBorder { + border-bottom: 1px dashed $turquoise; +} + +.u-circle { + @include border-radius(50%); +} + +.u-border--light { + border: 2px solid $fog; +} + +.u-border--edit { + border-top: 1px solid #d9d9d9; + border-right: 1px solid #d9d9d9; + border-left: 1px solid #d9d9d9; + border-bottom: 2px solid #d9d9d9; +} + +.u-border--bottom { + border-bottom: 1px solid rgba(black, 0.05); +} + +.u-underline { + color: inherit; + text-decoration: underline; + &:hover { + color: inherit; + @include opacity(0.8); + } +} + +.no-border {border: 0;} +.clear {clear: both;} +.clearl {clear: left;} +.clearr {clear: right;} + +.centered, .u-centered { + text-align: center !important; +} +.u-centered--shallow { + text-align: center; + & > * { + text-align: left; + } +} + +.u-noSelect { + -moz-user-select: none; + -webkit-user-select: none; + -ms-user-select:none; + user-select:none; + -o-user-select:none; +} + + +.u-lineHeight--1 { + line-height: 1; +} + +.u-halfOpacity { + @include opacity(0.5); +} +.u-noOpacity { + @include opacity(0); +} +.u-fade { + @include opacity(0.7); +} +.u-bold {font-weight: bold !important; } + +.u-small {font-size: 14px; } + +.u-ellipses { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.u-noTransition { + @include transition(none); +} + +.u-relative { + position: relative; +} + +.u-prepend { + position: relative; + &:before { + position: absolute; + color: $grey; + } +} + +.u-shadow { + @include box-shadow(0 1px 5px 0 rgba(0, 0, 0, 0.1)); +} + +.u-capitalize { + text-transform: capitalize; +} +.u-bitter { + font-family: 'Bitter'; +} +.u-strong { + font-weight: bold; +} + +.u-vAlign--bottom { + vertical-align: bottom; +} + +.u-fixed--bottom { + position: fixed; + bottom: 0; +} + +.u-security-notification { + display:flex; + text-align: left; + .u-security-icon { + padding-right:5px; + padding-top:1px; + } +} + +@media screen and (max-width: 600px) { + .u-hideIf--600 {display: none;} +} +@media screen and (max-width: 500px) { + .u-hideIf--500 {display: none;} +} +@media screen and (max-width: 400px) { + .u-hideIf--400 {display: none;} +} diff --git a/app/assets/stylesheets/common/vendor/froala_editor.css.scss b/app/assets/stylesheets/common/vendor/froala_editor.scss similarity index 100% rename from app/assets/stylesheets/common/vendor/froala_editor.css.scss rename to app/assets/stylesheets/common/vendor/froala_editor.scss diff --git a/app/assets/stylesheets/common/vendor/quill.bubble.css b/app/assets/stylesheets/common/vendor/quill.bubble.css deleted file mode 100644 index fa423d73e..000000000 --- a/app/assets/stylesheets/common/vendor/quill.bubble.css +++ /dev/null @@ -1,955 +0,0 @@ -/* License: LGPL-3.0-or-later */ -/*! - * Pasted in to simplify sprockets. Your mileage may vary - * - * Quill Editor v1.3.6 - * https://quilljs.com/ - * Copyright (c) 2014, Jason Chen - * Copyright (c) 2013, salesforce.com - */ -.ql-container { - box-sizing: border-box; - font-family: Helvetica, Arial, sans-serif; - font-size: 13px; - height: 100%; - margin: 0px; - position: relative; -} -.ql-container.ql-disabled .ql-tooltip { - visibility: hidden; -} -.ql-container.ql-disabled .ql-editor ul[data-checked] > li::before { - pointer-events: none; -} -.ql-clipboard { - left: -100000px; - height: 1px; - overflow-y: hidden; - position: absolute; - top: 50%; -} -.ql-clipboard p { - margin: 0; - padding: 0; -} -.ql-editor { - box-sizing: border-box; - line-height: 1.42; - height: 100%; - outline: none; - overflow-y: auto; - padding: 12px 15px; - tab-size: 4; - -moz-tab-size: 4; - text-align: left; - white-space: pre-wrap; - word-wrap: break-word; -} -.ql-editor > * { - cursor: text; -} -.ql-editor p, -.ql-editor ol, -.ql-editor ul, -.ql-editor pre, -.ql-editor blockquote, -.ql-editor h1, -.ql-editor h2, -.ql-editor h3, -.ql-editor h4, -.ql-editor h5, -.ql-editor h6 { - margin: 0; - padding: 0; - counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol, -.ql-editor ul { - padding-left: 1.5em; -} -.ql-editor ol > li, -.ql-editor ul > li { - list-style-type: none; -} -.ql-editor ul > li::before { - content: '\2022'; -} -.ql-editor ul[data-checked=true], -.ql-editor ul[data-checked=false] { - pointer-events: none; -} -.ql-editor ul[data-checked=true] > li *, -.ql-editor ul[data-checked=false] > li * { - pointer-events: all; -} -.ql-editor ul[data-checked=true] > li::before, -.ql-editor ul[data-checked=false] > li::before { - color: #777; - cursor: pointer; - pointer-events: all; -} -.ql-editor ul[data-checked=true] > li::before { - content: '\2611'; -} -.ql-editor ul[data-checked=false] > li::before { - content: '\2610'; -} -.ql-editor li::before { - display: inline-block; - white-space: nowrap; - width: 1.2em; -} -.ql-editor li:not(.ql-direction-rtl)::before { - margin-left: -1.5em; - margin-right: 0.3em; - text-align: right; -} -.ql-editor li.ql-direction-rtl::before { - margin-left: 0.3em; - margin-right: -1.5em; -} -.ql-editor ol li:not(.ql-direction-rtl), -.ql-editor ul li:not(.ql-direction-rtl) { - padding-left: 1.5em; -} -.ql-editor ol li.ql-direction-rtl, -.ql-editor ul li.ql-direction-rtl { - padding-right: 1.5em; -} -.ql-editor ol li { - counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; - counter-increment: list-0; -} -.ql-editor ol li:before { - content: counter(list-0, decimal) '. '; -} -.ql-editor ol li.ql-indent-1 { - counter-increment: list-1; -} -.ql-editor ol li.ql-indent-1:before { - content: counter(list-1, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-1 { - counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-2 { - counter-increment: list-2; -} -.ql-editor ol li.ql-indent-2:before { - content: counter(list-2, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-2 { - counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-3 { - counter-increment: list-3; -} -.ql-editor ol li.ql-indent-3:before { - content: counter(list-3, decimal) '. '; -} -.ql-editor ol li.ql-indent-3 { - counter-reset: list-4 list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-4 { - counter-increment: list-4; -} -.ql-editor ol li.ql-indent-4:before { - content: counter(list-4, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-4 { - counter-reset: list-5 list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-5 { - counter-increment: list-5; -} -.ql-editor ol li.ql-indent-5:before { - content: counter(list-5, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-5 { - counter-reset: list-6 list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-6 { - counter-increment: list-6; -} -.ql-editor ol li.ql-indent-6:before { - content: counter(list-6, decimal) '. '; -} -.ql-editor ol li.ql-indent-6 { - counter-reset: list-7 list-8 list-9; -} -.ql-editor ol li.ql-indent-7 { - counter-increment: list-7; -} -.ql-editor ol li.ql-indent-7:before { - content: counter(list-7, lower-alpha) '. '; -} -.ql-editor ol li.ql-indent-7 { - counter-reset: list-8 list-9; -} -.ql-editor ol li.ql-indent-8 { - counter-increment: list-8; -} -.ql-editor ol li.ql-indent-8:before { - content: counter(list-8, lower-roman) '. '; -} -.ql-editor ol li.ql-indent-8 { - counter-reset: list-9; -} -.ql-editor ol li.ql-indent-9 { - counter-increment: list-9; -} -.ql-editor ol li.ql-indent-9:before { - content: counter(list-9, decimal) '. '; -} -.ql-editor .ql-indent-1:not(.ql-direction-rtl) { - padding-left: 3em; -} -.ql-editor li.ql-indent-1:not(.ql-direction-rtl) { - padding-left: 4.5em; -} -.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right { - padding-right: 3em; -} -.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right { - padding-right: 4.5em; -} -.ql-editor .ql-indent-2:not(.ql-direction-rtl) { - padding-left: 6em; -} -.ql-editor li.ql-indent-2:not(.ql-direction-rtl) { - padding-left: 7.5em; -} -.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right { - padding-right: 6em; -} -.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right { - padding-right: 7.5em; -} -.ql-editor .ql-indent-3:not(.ql-direction-rtl) { - padding-left: 9em; -} -.ql-editor li.ql-indent-3:not(.ql-direction-rtl) { - padding-left: 10.5em; -} -.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right { - padding-right: 9em; -} -.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right { - padding-right: 10.5em; -} -.ql-editor .ql-indent-4:not(.ql-direction-rtl) { - padding-left: 12em; -} -.ql-editor li.ql-indent-4:not(.ql-direction-rtl) { - padding-left: 13.5em; -} -.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right { - padding-right: 12em; -} -.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right { - padding-right: 13.5em; -} -.ql-editor .ql-indent-5:not(.ql-direction-rtl) { - padding-left: 15em; -} -.ql-editor li.ql-indent-5:not(.ql-direction-rtl) { - padding-left: 16.5em; -} -.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right { - padding-right: 15em; -} -.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right { - padding-right: 16.5em; -} -.ql-editor .ql-indent-6:not(.ql-direction-rtl) { - padding-left: 18em; -} -.ql-editor li.ql-indent-6:not(.ql-direction-rtl) { - padding-left: 19.5em; -} -.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right { - padding-right: 18em; -} -.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right { - padding-right: 19.5em; -} -.ql-editor .ql-indent-7:not(.ql-direction-rtl) { - padding-left: 21em; -} -.ql-editor li.ql-indent-7:not(.ql-direction-rtl) { - padding-left: 22.5em; -} -.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right { - padding-right: 21em; -} -.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right { - padding-right: 22.5em; -} -.ql-editor .ql-indent-8:not(.ql-direction-rtl) { - padding-left: 24em; -} -.ql-editor li.ql-indent-8:not(.ql-direction-rtl) { - padding-left: 25.5em; -} -.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right { - padding-right: 24em; -} -.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right { - padding-right: 25.5em; -} -.ql-editor .ql-indent-9:not(.ql-direction-rtl) { - padding-left: 27em; -} -.ql-editor li.ql-indent-9:not(.ql-direction-rtl) { - padding-left: 28.5em; -} -.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right { - padding-right: 27em; -} -.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right { - padding-right: 28.5em; -} -.ql-editor .ql-video { - display: block; - max-width: 100%; -} -.ql-editor .ql-video.ql-align-center { - margin: 0 auto; -} -.ql-editor .ql-video.ql-align-right { - margin: 0 0 0 auto; -} -.ql-editor .ql-bg-black { - background-color: #000; -} -.ql-editor .ql-bg-red { - background-color: #e60000; -} -.ql-editor .ql-bg-orange { - background-color: #f90; -} -.ql-editor .ql-bg-yellow { - background-color: #ff0; -} -.ql-editor .ql-bg-green { - background-color: #008a00; -} -.ql-editor .ql-bg-blue { - background-color: #06c; -} -.ql-editor .ql-bg-purple { - background-color: #93f; -} -.ql-editor .ql-color-white { - color: #fff; -} -.ql-editor .ql-color-red { - color: #e60000; -} -.ql-editor .ql-color-orange { - color: #f90; -} -.ql-editor .ql-color-yellow { - color: #ff0; -} -.ql-editor .ql-color-green { - color: #008a00; -} -.ql-editor .ql-color-blue { - color: #06c; -} -.ql-editor .ql-color-purple { - color: #93f; -} -.ql-editor .ql-font-serif { - font-family: Georgia, Times New Roman, serif; -} -.ql-editor .ql-font-monospace { - font-family: Monaco, Courier New, monospace; -} -.ql-editor .ql-size-small { - font-size: 0.75em; -} -.ql-editor .ql-size-large { - font-size: 1.5em; -} -.ql-editor .ql-size-huge { - font-size: 2.5em; -} -.ql-editor .ql-direction-rtl { - direction: rtl; - text-align: inherit; -} -.ql-editor .ql-align-center { - text-align: center; -} -.ql-editor .ql-align-justify { - text-align: justify; -} -.ql-editor .ql-align-right { - text-align: right; -} -.ql-editor.ql-blank::before { - color: rgba(0,0,0,0.6); - content: attr(data-placeholder); - font-style: italic; - left: 15px; - pointer-events: none; - position: absolute; - right: 15px; -} -.ql-bubble.ql-toolbar:after, -.ql-bubble .ql-toolbar:after { - clear: both; - content: ''; - display: table; -} -.ql-bubble.ql-toolbar button, -.ql-bubble .ql-toolbar button { - background: none; - border: none; - cursor: pointer; - display: inline-block; - float: left; - height: 24px; - padding: 3px 5px; - width: 28px; -} -.ql-bubble.ql-toolbar button svg, -.ql-bubble .ql-toolbar button svg { - float: left; - height: 100%; -} -.ql-bubble.ql-toolbar button:active:hover, -.ql-bubble .ql-toolbar button:active:hover { - outline: none; -} -.ql-bubble.ql-toolbar input.ql-image[type=file], -.ql-bubble .ql-toolbar input.ql-image[type=file] { - display: none; -} -.ql-bubble.ql-toolbar button:hover, -.ql-bubble .ql-toolbar button:hover, -.ql-bubble.ql-toolbar button:focus, -.ql-bubble .ql-toolbar button:focus, -.ql-bubble.ql-toolbar button.ql-active, -.ql-bubble .ql-toolbar button.ql-active, -.ql-bubble.ql-toolbar .ql-picker-label:hover, -.ql-bubble .ql-toolbar .ql-picker-label:hover, -.ql-bubble.ql-toolbar .ql-picker-label.ql-active, -.ql-bubble .ql-toolbar .ql-picker-label.ql-active, -.ql-bubble.ql-toolbar .ql-picker-item:hover, -.ql-bubble .ql-toolbar .ql-picker-item:hover, -.ql-bubble.ql-toolbar .ql-picker-item.ql-selected, -.ql-bubble .ql-toolbar .ql-picker-item.ql-selected { - color: #fff; -} -.ql-bubble.ql-toolbar button:hover .ql-fill, -.ql-bubble .ql-toolbar button:hover .ql-fill, -.ql-bubble.ql-toolbar button:focus .ql-fill, -.ql-bubble .ql-toolbar button:focus .ql-fill, -.ql-bubble.ql-toolbar button.ql-active .ql-fill, -.ql-bubble .ql-toolbar button.ql-active .ql-fill, -.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-fill, -.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-fill, -.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-fill, -.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-fill, -.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-fill, -.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-fill, -.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-fill, -.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-fill, -.ql-bubble.ql-toolbar button:hover .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar button:hover .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar button:focus .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar button:focus .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar button.ql-active .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar button.ql-active .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, -.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, -.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill { - fill: #fff; -} -.ql-bubble.ql-toolbar button:hover .ql-stroke, -.ql-bubble .ql-toolbar button:hover .ql-stroke, -.ql-bubble.ql-toolbar button:focus .ql-stroke, -.ql-bubble .ql-toolbar button:focus .ql-stroke, -.ql-bubble.ql-toolbar button.ql-active .ql-stroke, -.ql-bubble .ql-toolbar button.ql-active .ql-stroke, -.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke, -.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke, -.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke, -.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke, -.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke, -.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke, -.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, -.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, -.ql-bubble.ql-toolbar button:hover .ql-stroke-miter, -.ql-bubble .ql-toolbar button:hover .ql-stroke-miter, -.ql-bubble.ql-toolbar button:focus .ql-stroke-miter, -.ql-bubble .ql-toolbar button:focus .ql-stroke-miter, -.ql-bubble.ql-toolbar button.ql-active .ql-stroke-miter, -.ql-bubble .ql-toolbar button.ql-active .ql-stroke-miter, -.ql-bubble.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, -.ql-bubble .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, -.ql-bubble.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, -.ql-bubble .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, -.ql-bubble.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, -.ql-bubble .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, -.ql-bubble.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, -.ql-bubble .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter { - stroke: #fff; -} -@media (pointer: coarse) { - .ql-bubble.ql-toolbar button:hover:not(.ql-active), - .ql-bubble .ql-toolbar button:hover:not(.ql-active) { - color: #ccc; - } - .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-fill, - .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-fill, - .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, - .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill { - fill: #ccc; - } - .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke, - .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke, - .ql-bubble.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, - .ql-bubble .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter { - stroke: #ccc; - } -} -.ql-bubble { - box-sizing: border-box; -} -.ql-bubble * { - box-sizing: border-box; -} -.ql-bubble .ql-hidden { - display: none; -} -.ql-bubble .ql-out-bottom, -.ql-bubble .ql-out-top { - visibility: hidden; -} -.ql-bubble .ql-tooltip { - position: absolute; - transform: translateY(10px); -} -.ql-bubble .ql-tooltip a { - cursor: pointer; - text-decoration: none; -} -.ql-bubble .ql-tooltip.ql-flip { - transform: translateY(-10px); -} -.ql-bubble .ql-formats { - display: inline-block; - vertical-align: middle; -} -.ql-bubble .ql-formats:after { - clear: both; - content: ''; - display: table; -} -.ql-bubble .ql-stroke { - fill: none; - stroke: #ccc; - stroke-linecap: round; - stroke-linejoin: round; - stroke-width: 2; -} -.ql-bubble .ql-stroke-miter { - fill: none; - stroke: #ccc; - stroke-miterlimit: 10; - stroke-width: 2; -} -.ql-bubble .ql-fill, -.ql-bubble .ql-stroke.ql-fill { - fill: #ccc; -} -.ql-bubble .ql-empty { - fill: none; -} -.ql-bubble .ql-even { - fill-rule: evenodd; -} -.ql-bubble .ql-thin, -.ql-bubble .ql-stroke.ql-thin { - stroke-width: 1; -} -.ql-bubble .ql-transparent { - opacity: 0.4; -} -.ql-bubble .ql-direction svg:last-child { - display: none; -} -.ql-bubble .ql-direction.ql-active svg:last-child { - display: inline; -} -.ql-bubble .ql-direction.ql-active svg:first-child { - display: none; -} -.ql-bubble .ql-editor h1 { - font-size: 2em; -} -.ql-bubble .ql-editor h2 { - font-size: 1.5em; -} -.ql-bubble .ql-editor h3 { - font-size: 1.17em; -} -.ql-bubble .ql-editor h4 { - font-size: 1em; -} -.ql-bubble .ql-editor h5 { - font-size: 0.83em; -} -.ql-bubble .ql-editor h6 { - font-size: 0.67em; -} -.ql-bubble .ql-editor a { - text-decoration: underline; -} -.ql-bubble .ql-editor blockquote { - border-left: 4px solid #ccc; - margin-bottom: 5px; - margin-top: 5px; - padding-left: 16px; -} -.ql-bubble .ql-editor code, -.ql-bubble .ql-editor pre { - background-color: #f0f0f0; - border-radius: 3px; -} -.ql-bubble .ql-editor pre { - white-space: pre-wrap; - margin-bottom: 5px; - margin-top: 5px; - padding: 5px 10px; -} -.ql-bubble .ql-editor code { - font-size: 85%; - padding: 2px 4px; -} -.ql-bubble .ql-editor pre.ql-syntax { - background-color: #23241f; - color: #f8f8f2; - overflow: visible; -} -.ql-bubble .ql-editor img { - max-width: 100%; -} -.ql-bubble .ql-picker { - color: #ccc; - display: inline-block; - float: left; - font-size: 14px; - font-weight: 500; - height: 24px; - position: relative; - vertical-align: middle; -} -.ql-bubble .ql-picker-label { - cursor: pointer; - display: inline-block; - height: 100%; - padding-left: 8px; - padding-right: 2px; - position: relative; - width: 100%; -} -.ql-bubble .ql-picker-label::before { - display: inline-block; - line-height: 22px; -} -.ql-bubble .ql-picker-options { - background-color: #444; - display: none; - min-width: 100%; - padding: 4px 8px; - position: absolute; - white-space: nowrap; -} -.ql-bubble .ql-picker-options .ql-picker-item { - cursor: pointer; - display: block; - padding-bottom: 5px; - padding-top: 5px; -} -.ql-bubble .ql-picker.ql-expanded .ql-picker-label { - color: #777; - z-index: 2; -} -.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-fill { - fill: #777; -} -.ql-bubble .ql-picker.ql-expanded .ql-picker-label .ql-stroke { - stroke: #777; -} -.ql-bubble .ql-picker.ql-expanded .ql-picker-options { - display: block; - margin-top: -1px; - top: 100%; - z-index: 1; -} -.ql-bubble .ql-color-picker, -.ql-bubble .ql-icon-picker { - width: 28px; -} -.ql-bubble .ql-color-picker .ql-picker-label, -.ql-bubble .ql-icon-picker .ql-picker-label { - padding: 2px 4px; -} -.ql-bubble .ql-color-picker .ql-picker-label svg, -.ql-bubble .ql-icon-picker .ql-picker-label svg { - right: 4px; -} -.ql-bubble .ql-icon-picker .ql-picker-options { - padding: 4px 0px; -} -.ql-bubble .ql-icon-picker .ql-picker-item { - height: 24px; - width: 24px; - padding: 2px 4px; -} -.ql-bubble .ql-color-picker .ql-picker-options { - padding: 3px 5px; - width: 152px; -} -.ql-bubble .ql-color-picker .ql-picker-item { - border: 1px solid transparent; - float: left; - height: 16px; - margin: 2px; - padding: 0px; - width: 16px; -} -.ql-bubble .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { - position: absolute; - margin-top: -9px; - right: 0; - top: 50%; - width: 18px; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-bubble .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-bubble .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, -.ql-bubble .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before { - content: attr(data-label); -} -.ql-bubble .ql-picker.ql-header { - width: 98px; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item::before { - content: 'Normal'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="1"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { - content: 'Heading 1'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="2"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { - content: 'Heading 2'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="3"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { - content: 'Heading 3'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="4"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { - content: 'Heading 4'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="5"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { - content: 'Heading 5'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-label[data-value="6"]::before, -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { - content: 'Heading 6'; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="1"]::before { - font-size: 2em; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="2"]::before { - font-size: 1.5em; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="3"]::before { - font-size: 1.17em; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="4"]::before { - font-size: 1em; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="5"]::before { - font-size: 0.83em; -} -.ql-bubble .ql-picker.ql-header .ql-picker-item[data-value="6"]::before { - font-size: 0.67em; -} -.ql-bubble .ql-picker.ql-font { - width: 108px; -} -.ql-bubble .ql-picker.ql-font .ql-picker-label::before, -.ql-bubble .ql-picker.ql-font .ql-picker-item::before { - content: 'Sans Serif'; -} -.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=serif]::before, -.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before { - content: 'Serif'; -} -.ql-bubble .ql-picker.ql-font .ql-picker-label[data-value=monospace]::before, -.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before { - content: 'Monospace'; -} -.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=serif]::before { - font-family: Georgia, Times New Roman, serif; -} -.ql-bubble .ql-picker.ql-font .ql-picker-item[data-value=monospace]::before { - font-family: Monaco, Courier New, monospace; -} -.ql-bubble .ql-picker.ql-size { - width: 98px; -} -.ql-bubble .ql-picker.ql-size .ql-picker-label::before, -.ql-bubble .ql-picker.ql-size .ql-picker-item::before { - content: 'Normal'; -} -.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=small]::before, -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before { - content: 'Small'; -} -.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=large]::before, -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before { - content: 'Large'; -} -.ql-bubble .ql-picker.ql-size .ql-picker-label[data-value=huge]::before, -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before { - content: 'Huge'; -} -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=small]::before { - font-size: 10px; -} -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=large]::before { - font-size: 18px; -} -.ql-bubble .ql-picker.ql-size .ql-picker-item[data-value=huge]::before { - font-size: 32px; -} -.ql-bubble .ql-color-picker.ql-background .ql-picker-item { - background-color: #fff; -} -.ql-bubble .ql-color-picker.ql-color .ql-picker-item { - background-color: #000; -} -.ql-bubble .ql-toolbar .ql-formats { - margin: 8px 12px 8px 0px; -} -.ql-bubble .ql-toolbar .ql-formats:first-child { - margin-left: 12px; -} -.ql-bubble .ql-color-picker svg { - margin: 1px; -} -.ql-bubble .ql-color-picker .ql-picker-item.ql-selected, -.ql-bubble .ql-color-picker .ql-picker-item:hover { - border-color: #fff; -} -.ql-bubble .ql-tooltip { - background-color: #444; - border-radius: 25px; - color: #fff; -} -.ql-bubble .ql-tooltip-arrow { - border-left: 6px solid transparent; - border-right: 6px solid transparent; - content: " "; - display: block; - left: 50%; - margin-left: -6px; - position: absolute; -} -.ql-bubble .ql-tooltip:not(.ql-flip) .ql-tooltip-arrow { - border-bottom: 6px solid #444; - top: -6px; -} -.ql-bubble .ql-tooltip.ql-flip .ql-tooltip-arrow { - border-top: 6px solid #444; - bottom: -6px; -} -.ql-bubble .ql-tooltip.ql-editing .ql-tooltip-editor { - display: block; -} -.ql-bubble .ql-tooltip.ql-editing .ql-formats { - visibility: hidden; -} -.ql-bubble .ql-tooltip-editor { - display: none; -} -.ql-bubble .ql-tooltip-editor input[type=text] { - background: transparent; - border: none; - color: #fff; - font-size: 13px; - height: 100%; - outline: none; - padding: 10px 20px; - position: absolute; - width: 100%; -} -.ql-bubble .ql-tooltip-editor a { - top: 10px; - position: absolute; - right: 20px; -} -.ql-bubble .ql-tooltip-editor a:before { - color: #ccc; - content: "\D7"; - font-size: 16px; - font-weight: bold; -} -.ql-container.ql-bubble:not(.ql-disabled) a { - position: relative; - white-space: nowrap; -} -.ql-container.ql-bubble:not(.ql-disabled) a::before { - background-color: #444; - border-radius: 15px; - top: -5px; - font-size: 12px; - color: #fff; - content: attr(href); - font-weight: normal; - overflow: hidden; - padding: 5px 15px; - text-decoration: none; - z-index: 1; -} -.ql-container.ql-bubble:not(.ql-disabled) a::after { - border-top: 6px solid #444; - border-left: 6px solid transparent; - border-right: 6px solid transparent; - top: 0; - content: " "; - height: 0; - width: 0; -} -.ql-container.ql-bubble:not(.ql-disabled) a::before, -.ql-container.ql-bubble:not(.ql-disabled) a::after { - left: 0; - margin-left: 50%; - position: absolute; - transform: translate(-50%, -100%); - transition: visibility 0s ease 200ms; - visibility: hidden; -} -.ql-container.ql-bubble:not(.ql-disabled) a:hover::before, -.ql-container.ql-bubble:not(.ql-disabled) a:hover::after { - visibility: visible; -} diff --git a/app/assets/stylesheets/common/z_indices.css.scss b/app/assets/stylesheets/common/z_indices.scss similarity index 100% rename from app/assets/stylesheets/common/z_indices.css.scss rename to app/assets/stylesheets/common/z_indices.scss diff --git a/app/assets/stylesheets/components/activity_feed.css.scss b/app/assets/stylesheets/components/activity_feed.scss similarity index 100% rename from app/assets/stylesheets/components/activity_feed.css.scss rename to app/assets/stylesheets/components/activity_feed.scss diff --git a/app/assets/stylesheets/components/admin_sidebar.css.scss b/app/assets/stylesheets/components/admin_sidebar.scss similarity index 100% rename from app/assets/stylesheets/components/admin_sidebar.css.scss rename to app/assets/stylesheets/components/admin_sidebar.scss diff --git a/app/assets/stylesheets/components/admin_top_nav.css.scss b/app/assets/stylesheets/components/admin_top_nav.scss similarity index 100% rename from app/assets/stylesheets/components/admin_top_nav.css.scss rename to app/assets/stylesheets/components/admin_top_nav.scss diff --git a/app/assets/stylesheets/components/animations.css.scss b/app/assets/stylesheets/components/animations.css.scss deleted file mode 100644 index b66df2361..000000000 --- a/app/assets/stylesheets/components/animations.css.scss +++ /dev/null @@ -1,44 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@mixin animation($animate...) { - $max: length($animate); - $animations: ''; - - @for $i from 1 through $max { - $animations: #{$animations + nth($animate, $i)}; - - @if $i < $max { - $animations: #{$animations + ", "}; - } - } - -webkit-animation: $animations; - -moz-animation: $animations; - -o-animation: $animations; - animation: $animations; -} - -@mixin keyframes($animationName) { - @-webkit-keyframes #{$animationName} { - @content; - } - @-moz-keyframes #{$animationName} { - @content; - } - @-o-keyframes #{$animationName} { - @content; - } - @keyframes #{$animationName} { - @content; - } -} - -// Using the mixins looks like this: -// @include keyframes(move-the-object) { -// 0% { left: 100px; } -// 100% { left: 200px; } -// } - -// .object-to-animate { -// @include animation('move-the-object .5s 1', 'move-the-object-again .5s 1 .5s'); -// } - -// credit: http://joshbroton.com/quick-fix-sass-mixins-for-css-keyframe-animations/ diff --git a/app/assets/stylesheets/components/animations.scss b/app/assets/stylesheets/components/animations.scss new file mode 100644 index 000000000..000711d8c --- /dev/null +++ b/app/assets/stylesheets/components/animations.scss @@ -0,0 +1,35 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@mixin animation($animate...) { + $max: length($animate); + $animations: ''; + + @for $i from 1 through $max { + $animations: #{$animations + nth($animate, $i)}; + + @if $i < $max { + $animations: #{$animations + ", "}; + } + } + -webkit-animation: $animations; + -moz-animation: $animations; + -o-animation: $animations; + animation: $animations; +} + +@mixin keyframes($animationName) { + @keyframes #{$animationName} { + @content; + } +} + +// Using the mixins looks like this: +// @include keyframes(move-the-object) { +// 0% { left: 100px; } +// 100% { left: 200px; } +// } + +// .object-to-animate { +// @include animation('move-the-object .5s 1', 'move-the-object-again .5s 1 .5s'); +// } + +// credit: http://joshbroton.com/quick-fix-sass-mixins-for-css-keyframe-animations/ diff --git a/app/assets/stylesheets/components/announcement_bar.css.scss b/app/assets/stylesheets/components/announcement_bar.scss similarity index 100% rename from app/assets/stylesheets/components/announcement_bar.css.scss rename to app/assets/stylesheets/components/announcement_bar.scss diff --git a/app/assets/stylesheets/components/app_loading_bar.css.scss b/app/assets/stylesheets/components/app_loading_bar.css.scss deleted file mode 100644 index 8c08cfddc..000000000 --- a/app/assets/stylesheets/components/app_loading_bar.css.scss +++ /dev/null @@ -1,61 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -@-webkit-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} - -@-moz-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -@-ms-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -@-o-keyframes progress-bar-stripes{ - from{background-position:0 0} - to{background-position:40px 0} -} -@keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} - -.progressBar--outer { - position: fixed; - bottom: 0; - left: 0; - width: 100%; - height: 15px; - margin: 0; - padding: 0; - overflow: hidden; - background-color: #f7f7f7; -} - -.progressBar--inner { - width: 100%; - height: 100%; - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; - background-color: $bluegrass; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(white,0.3)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(white,0.3)), color-stop(0.75, rgba(white,0.3)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; - background-repeat: repeat-x; - -webkit-transition: width .6s ease; - -moz-transition: width .6s ease; - -o-transition: width .6s ease; - transition: width .6s ease; -} diff --git a/app/assets/stylesheets/components/app_loading_bar.scss b/app/assets/stylesheets/components/app_loading_bar.scss new file mode 100644 index 000000000..2f8c3f0d7 --- /dev/null +++ b/app/assets/stylesheets/components/app_loading_bar.scss @@ -0,0 +1,44 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +@keyframes progress-bar-stripes{ + from{background-position:40px 0} + to{background-position:0 0} +} + +.progressBar--outer { + position: fixed; + bottom: 0; + left: 0; + width: 100%; + height: 15px; + margin: 0; + padding: 0; + overflow: hidden; + background-color: #f7f7f7; +} + +.progressBar--inner { + width: 100%; + height: 100%; + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; + background-color: $bluegrass; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(white,0.3)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(white,0.3)), color-stop(0.75, rgba(white,0.3)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; + background-repeat: repeat-x; + -webkit-transition: width .6s ease; + -moz-transition: width .6s ease; + -o-transition: width .6s ease; + transition: width .6s ease; +} diff --git a/app/assets/stylesheets/components/arrows.css.scss b/app/assets/stylesheets/components/arrows.scss similarity index 100% rename from app/assets/stylesheets/components/arrows.css.scss rename to app/assets/stylesheets/components/arrows.scss diff --git a/app/assets/stylesheets/components/better_browser.css.scss b/app/assets/stylesheets/components/better_browser.scss similarity index 100% rename from app/assets/stylesheets/components/better_browser.css.scss rename to app/assets/stylesheets/components/better_browser.scss diff --git a/app/assets/stylesheets/components/browser_border.css.scss b/app/assets/stylesheets/components/browser_border.scss similarity index 100% rename from app/assets/stylesheets/components/browser_border.css.scss rename to app/assets/stylesheets/components/browser_border.scss diff --git a/app/assets/stylesheets/components/browsers_illustration.css.scss b/app/assets/stylesheets/components/browsers_illustration.scss similarity index 100% rename from app/assets/stylesheets/components/browsers_illustration.css.scss rename to app/assets/stylesheets/components/browsers_illustration.scss diff --git a/app/assets/stylesheets/components/bulk_actions.css.scss b/app/assets/stylesheets/components/bulk_actions.scss similarity index 100% rename from app/assets/stylesheets/components/bulk_actions.css.scss rename to app/assets/stylesheets/components/bulk_actions.scss diff --git a/app/assets/stylesheets/components/buttons.css.scss b/app/assets/stylesheets/components/buttons.scss similarity index 100% rename from app/assets/stylesheets/components/buttons.css.scss rename to app/assets/stylesheets/components/buttons.scss diff --git a/app/assets/stylesheets/components/campaign_preview_small.css.scss b/app/assets/stylesheets/components/campaign_preview_small.scss similarity index 100% rename from app/assets/stylesheets/components/campaign_preview_small.css.scss rename to app/assets/stylesheets/components/campaign_preview_small.scss diff --git a/app/assets/stylesheets/components/cards.css.scss b/app/assets/stylesheets/components/cards.css.scss deleted file mode 100755 index 263db6b47..000000000 --- a/app/assets/stylesheets/components/cards.css.scss +++ /dev/null @@ -1,35 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -.cardForm { - max-width: 380px; - margin: 0 auto; -} -.cardForm input, -.cardForm select { - margin: 0; -} -.cardForm .progress { - width: 100%; -} -.cardForm .parsley-error-list { - display: none; -} -.cardForm .security-code-image, -.cardForm .card-logos { - position: absolute; - top: 5px; - right: 5px; -} -.cardForm .card-logos { - width: 32px; - height: 21px; - background-image: url('/assets/graphics/credit-card-logos.png'); - @include opacity(0); - - &.americanexpress, &.visa, &.discovercard, &.mastercard { @include opacity(1); } - &.americanexpress { background-position: 0; } - &.visa { background-position: 32px; } - &.mastercard { background-position: 64px; } - &.discovercard { background-position: 96px; } -} diff --git a/app/assets/stylesheets/components/cards.scss.erb b/app/assets/stylesheets/components/cards.scss.erb new file mode 100755 index 000000000..a3828397e --- /dev/null +++ b/app/assets/stylesheets/components/cards.scss.erb @@ -0,0 +1,35 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +.cardForm { + max-width: 380px; + margin: 0 auto; +} +.cardForm input, +.cardForm select { + margin: 0; +} +.cardForm .progress { + width: 100%; +} +.cardForm .parsley-error-list { + display: none; +} +.cardForm .security-code-image, +.cardForm .card-logos { + position: absolute; + top: 5px; + right: 5px; +} +.cardForm .card-logos { + width: 32px; + height: 21px; + background-image: url('<%= asset_path '/graphics/credit-card-logos.png'%>'); + @include opacity(0); + + &.americanexpress, &.visa, &.discovercard, &.mastercard { @include opacity(1); } + &.americanexpress { background-position: 0; } + &.visa { background-position: 32px; } + &.mastercard { background-position: 64px; } + &.discovercard { background-position: 96px; } +} diff --git a/app/assets/stylesheets/components/carousel.css.scss b/app/assets/stylesheets/components/carousel.scss similarity index 100% rename from app/assets/stylesheets/components/carousel.css.scss rename to app/assets/stylesheets/components/carousel.scss diff --git a/app/assets/stylesheets/components/cc_pattern.css.scss b/app/assets/stylesheets/components/cc_pattern.css.scss deleted file mode 100644 index 9ca29dda1..000000000 --- a/app/assets/stylesheets/components/cc_pattern.css.scss +++ /dev/null @@ -1,28 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -.ccPattern { - position: absolute; - top: 0; - left: 0; - width: 100%; - height: 100%; -} -.ccPattern:after, -.ccPattern:before { - position: absolute; - content: ''; - width: 100%; - left: 0; - bottom: 0; -} -.ccPattern:after { - height: 100%; - background-image: url('/assets/patterns/features-nodes.svg'); - background-size: contain; - @include opacity(0.3); - } -.ccPattern:before { - height: 60%; - @include gradient(top, $trans, white); -} diff --git a/app/assets/stylesheets/components/cc_pattern.scss.erb b/app/assets/stylesheets/components/cc_pattern.scss.erb new file mode 100644 index 000000000..16b4ee913 --- /dev/null +++ b/app/assets/stylesheets/components/cc_pattern.scss.erb @@ -0,0 +1,28 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +.ccPattern { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +.ccPattern:after, +.ccPattern:before { + position: absolute; + content: ''; + width: 100%; + left: 0; + bottom: 0; +} +.ccPattern:after { + height: 100%; + background-image: url('<%= asset_path 'patterns/features-nodes.svg'%>'); + background-size: contain; + @include opacity(0.3); + } +.ccPattern:before { + height: 60%; + @include gradient(top, $trans, white); +} diff --git a/app/assets/stylesheets/components/checklist.css.scss b/app/assets/stylesheets/components/checklist.scss similarity index 100% rename from app/assets/stylesheets/components/checklist.css.scss rename to app/assets/stylesheets/components/checklist.scss diff --git a/app/assets/stylesheets/components/circle_text.css.scss b/app/assets/stylesheets/components/circle_text.scss similarity index 100% rename from app/assets/stylesheets/components/circle_text.css.scss rename to app/assets/stylesheets/components/circle_text.scss diff --git a/app/assets/stylesheets/components/confirmation.css.scss b/app/assets/stylesheets/components/confirmation.scss similarity index 100% rename from app/assets/stylesheets/components/confirmation.css.scss rename to app/assets/stylesheets/components/confirmation.scss diff --git a/app/assets/stylesheets/components/container.css.scss b/app/assets/stylesheets/components/container.scss similarity index 100% rename from app/assets/stylesheets/components/container.css.scss rename to app/assets/stylesheets/components/container.scss diff --git a/app/assets/stylesheets/components/decorative.css.scss b/app/assets/stylesheets/components/decorative.scss similarity index 100% rename from app/assets/stylesheets/components/decorative.css.scss rename to app/assets/stylesheets/components/decorative.scss diff --git a/app/assets/stylesheets/components/draggable.css.scss b/app/assets/stylesheets/components/draggable.css.scss deleted file mode 100644 index f6250c7cd..000000000 --- a/app/assets/stylesheets/components/draggable.css.scss +++ /dev/null @@ -1,52 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -.gu-mirror { - position: fixed !important; - margin: 0 !important; - z-index: 9999 !important; - @include box-shadow(0 0 10px 0 rgba(black, 0.15)); - background: white; - border: 1px solid rgba(black, 0.1); -} - -.gu-hide { - display: none !important; -} - -.gu-unselectable { - @include noselect; -} - -.gu-transit { - background: rgba(black, 0.1); - @include opacity(0.3); -} - -.gu-mirror { - display: table; - vertical-align: middle; -} - -.gu-mirror button, -.gu-mirror a { - display: none; -} - -.draggable-item { - cursor: pointer; - @include noselect; -} - -.draggable-item:hover .draggable-grip { - @include opacity(0.5); -} - -.draggable-grip { - display: inline-block; - height: 100%; - width: 12px; - background-image: url(/assets/graphics/icon-grip.svg); - background-repeat: repeat-y; - background-size: 8px; - @include opacity(0.1); -} - diff --git a/app/assets/stylesheets/components/draggable.scss.erb b/app/assets/stylesheets/components/draggable.scss.erb new file mode 100644 index 000000000..c4633c62f --- /dev/null +++ b/app/assets/stylesheets/components/draggable.scss.erb @@ -0,0 +1,52 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +.gu-mirror { + position: fixed !important; + margin: 0 !important; + z-index: 9999 !important; + @include box-shadow(0 0 10px 0 rgba(black, 0.15)); + background: white; + border: 1px solid rgba(black, 0.1); +} + +.gu-hide { + display: none !important; +} + +.gu-unselectable { + @include noselect; +} + +.gu-transit { + background: rgba(black, 0.1); + @include opacity(0.3); +} + +.gu-mirror { + display: table; + vertical-align: middle; +} + +.gu-mirror button, +.gu-mirror a { + display: none; +} + +.draggable-item { + cursor: pointer; + @include noselect; +} + +.draggable-item:hover .draggable-grip { + @include opacity(0.5); +} + +.draggable-grip { + display: inline-block; + height: 100%; + width: 12px; + background-image: url('<%= asset_path 'graphics/icon-grip.svg' %>'); + background-repeat: repeat-y; + background-size: 8px; + @include opacity(0.1); +} + diff --git a/app/assets/stylesheets/components/drop_down.css.scss b/app/assets/stylesheets/components/drop_down.scss similarity index 100% rename from app/assets/stylesheets/components/drop_down.css.scss rename to app/assets/stylesheets/components/drop_down.scss diff --git a/app/assets/stylesheets/components/event_preview_small.css.scss b/app/assets/stylesheets/components/event_preview_small.scss similarity index 100% rename from app/assets/stylesheets/components/event_preview_small.css.scss rename to app/assets/stylesheets/components/event_preview_small.scss diff --git a/app/assets/stylesheets/components/fade_in.css.scss b/app/assets/stylesheets/components/fade_in.scss similarity index 100% rename from app/assets/stylesheets/components/fade_in.css.scss rename to app/assets/stylesheets/components/fade_in.scss diff --git a/app/assets/stylesheets/components/fee_box.css.scss b/app/assets/stylesheets/components/fee_box.scss similarity index 100% rename from app/assets/stylesheets/components/fee_box.css.scss rename to app/assets/stylesheets/components/fee_box.scss diff --git a/app/assets/stylesheets/components/ff_modal.css.scss b/app/assets/stylesheets/components/ff_modal.scss similarity index 100% rename from app/assets/stylesheets/components/ff_modal.css.scss rename to app/assets/stylesheets/components/ff_modal.scss diff --git a/app/assets/stylesheets/components/fixed_top_action.css.scss b/app/assets/stylesheets/components/fixed_top_action.scss similarity index 100% rename from app/assets/stylesheets/components/fixed_top_action.css.scss rename to app/assets/stylesheets/components/fixed_top_action.scss diff --git a/app/assets/stylesheets/components/focal_point.css.scss b/app/assets/stylesheets/components/focal_point.scss similarity index 100% rename from app/assets/stylesheets/components/focal_point.css.scss rename to app/assets/stylesheets/components/focal_point.scss diff --git a/app/assets/stylesheets/components/footer.css.scss b/app/assets/stylesheets/components/footer.css.scss deleted file mode 100644 index c97c50372..000000000 --- a/app/assets/stylesheets/components/footer.css.scss +++ /dev/null @@ -1,65 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'components/press_row'; - -.globalFooter--mosaic { - background: $mosaic; - width: 100%; - height: 4px; -} -.globalFooter { - width: 100%; - background: $blue-grey; - position: relative; - @include mosaic(bottom, 4px); -} -.globalFooter-guest.container { padding: 30px 15px 15px 15px; } -.globalFooter * { color: white; } -.globalFooter a:not(.globalFooter-sub-item--social) { - @include setColorAndHover(white); - border-bottom: 1px solid rgba(white, 0.2); -} -.globalFooter-title { font-weight: bold; } -.globalFooter li:not(.globalFooter-title) { - margin-top: 5px; - padding: 0 0 5px 0; - font-size: 15px; -} - -// footer sub -.globalFooter-sub { - background: rgba(black, 0.06); - padding: 20px 0 25px 0; -} -.globalFooter-sub .container { - padding: 0 10px; -} -.globalFooter-sub-item, -.globalFooter-sub-item--social { - font-size: 14px; - margin-right: 10px; -} -.globalFooter-sub-item--social i.fa { - @include opacity(0.9); - text-align: center; - margin: 0; - width: 25px; - line-height: 25px; - @include border-radius(50%); - @include setBackgroundAndHover(rgba(white, 0.9)); - -} -.globalFooter-sub-item--social i.fa-facebook { color: $facebook; } -.globalFooter-sub-item--social i.fa-twitter { color: $twitter; } -.globalFooter-sub-item--social i.fa-google { color: $google; } -.globalFooter-sub-commitchange { - @include bitter; - font-size: 17px; - font-weight: bold; - margin-right: 15px; -} -.globalFooter-sub img { - vertical-align: text-bottom; - width: 25px; -} - diff --git a/app/assets/stylesheets/components/footer.scss b/app/assets/stylesheets/components/footer.scss new file mode 100644 index 000000000..7f547dc80 --- /dev/null +++ b/app/assets/stylesheets/components/footer.scss @@ -0,0 +1,64 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +.globalFooter--mosaic { + background: $mosaic; + width: 100%; + height: 4px; +} +.globalFooter { + width: 100%; + background: $blue-grey; + position: relative; + @include mosaic(bottom, 4px); +} +.globalFooter-guest.container { padding: 30px 15px 15px 15px; } +.globalFooter * { color: white; } +.globalFooter a:not(.globalFooter-sub-item--social) { + @include setColorAndHover(white); + border-bottom: 1px solid rgba(white, 0.2); +} +.globalFooter-title { font-weight: bold; } +.globalFooter li:not(.globalFooter-title) { + margin-top: 5px; + padding: 0 0 5px 0; + font-size: 15px; +} + +// footer sub +.globalFooter-sub { + background: rgba(black, 0.06); + padding: 20px 0 25px 0; +} +.globalFooter-sub .container { + padding: 0 10px; +} +.globalFooter-sub-item, +.globalFooter-sub-item--social { + font-size: 14px; + margin-right: 10px; +} +.globalFooter-sub-item--social i.fa { + @include opacity(0.9); + text-align: center; + margin: 0; + width: 25px; + line-height: 25px; + @include border-radius(50%); + @include setBackgroundAndHover(rgba(white, 0.9)); + +} +.globalFooter-sub-item--social i.fa-facebook { color: $facebook; } +.globalFooter-sub-item--social i.fa-twitter { color: $twitter; } +.globalFooter-sub-item--social i.fa-google { color: $google; } +.globalFooter-sub-commitchange { + @include bitter; + font-size: 17px; + font-weight: bold; + margin-right: 15px; +} +.globalFooter-sub img { + vertical-align: text-bottom; + width: 25px; +} + diff --git a/app/assets/stylesheets/components/forms.css.scss b/app/assets/stylesheets/components/forms.scss similarity index 100% rename from app/assets/stylesheets/components/forms.css.scss rename to app/assets/stylesheets/components/forms.scss diff --git a/app/assets/stylesheets/components/full_features.css.scss b/app/assets/stylesheets/components/full_features.scss similarity index 100% rename from app/assets/stylesheets/components/full_features.css.scss rename to app/assets/stylesheets/components/full_features.scss diff --git a/app/assets/stylesheets/components/full_screen_loading.css.scss b/app/assets/stylesheets/components/full_screen_loading.scss similarity index 100% rename from app/assets/stylesheets/components/full_screen_loading.css.scss rename to app/assets/stylesheets/components/full_screen_loading.scss diff --git a/app/assets/stylesheets/components/giving_indicator.css.scss b/app/assets/stylesheets/components/giving_indicator.scss similarity index 100% rename from app/assets/stylesheets/components/giving_indicator.css.scss rename to app/assets/stylesheets/components/giving_indicator.scss diff --git a/app/assets/stylesheets/components/google_maps.css.scss b/app/assets/stylesheets/components/google_maps.scss similarity index 100% rename from app/assets/stylesheets/components/google_maps.css.scss rename to app/assets/stylesheets/components/google_maps.scss diff --git a/app/assets/stylesheets/components/headers.css.scss b/app/assets/stylesheets/components/headers.scss similarity index 100% rename from app/assets/stylesheets/components/headers.css.scss rename to app/assets/stylesheets/components/headers.scss diff --git a/app/assets/stylesheets/components/help_box.css.scss b/app/assets/stylesheets/components/help_box.scss similarity index 100% rename from app/assets/stylesheets/components/help_box.css.scss rename to app/assets/stylesheets/components/help_box.scss diff --git a/app/assets/stylesheets/components/identity_verification.css.scss b/app/assets/stylesheets/components/identity_verification.css.scss deleted file mode 100644 index 5b55462c4..000000000 --- a/app/assets/stylesheets/components/identity_verification.css.scss +++ /dev/null @@ -1,10 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ - -#identityVerificationModal fieldset { - padding: 10px 10px 10px 0; -} - -#identityVerificationModal input { - margin-bottom: 0; -} - diff --git a/app/assets/stylesheets/components/image_header.css.scss b/app/assets/stylesheets/components/image_header.scss similarity index 100% rename from app/assets/stylesheets/components/image_header.css.scss rename to app/assets/stylesheets/components/image_header.scss diff --git a/app/assets/stylesheets/components/info_card.css.scss b/app/assets/stylesheets/components/info_card.scss similarity index 100% rename from app/assets/stylesheets/components/info_card.css.scss rename to app/assets/stylesheets/components/info_card.scss diff --git a/app/assets/stylesheets/components/inputs.css.scss b/app/assets/stylesheets/components/inputs.css.scss deleted file mode 100644 index 5f236d1e5..000000000 --- a/app/assets/stylesheets/components/inputs.css.scss +++ /dev/null @@ -1,323 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -@mixin basicInputs { - input[type="text"], input, input[type="email"], input[type="number"], input[type="password"], input[type="tel"], select { - @content; - } -} - -@mixin inputStyleReset { - color: $charcoal; - padding: 4px 8px; - font-size: 16px; // inputs should all be 16px to prevent zooming on mobile - @include border-radius(0); - @include transition(border-color 0.2s ease-out); - border-top: 1px solid lighten($grey, 35%); - border-right: 1px solid lighten($grey, 35%); - border-left: 1px solid lighten($grey, 35%); - border-bottom: 2px solid lighten($grey, 35%); - margin-bottom: 12px; - &:focus { - border-bottom: 2px solid lighten($grey, 15%); - } -} - -@mixin clickableLabel { - cursor: pointer; - display: inline-block; - position: relative; - font-weight: normal; - @include setColorAndHover($charcoal); - margin: 0; -} - - -@include basicInputs { - @include inputStyleReset; - &.input--50 { max-width: 50px; } - &.input--100 { max-width: 100px; } - &.input--150 { max-width: 150px; } - &.input--175 { max-width: 175px; } - &.input--200 { max-width: 200px; } - &.input--250 { max-width: 250px; } - &.input--300 { max-width: 300px; } - &.input--400 { max-width: 400px; } - &.input--half { max-width: 48%; } - &.input--mini { width: 3em; } - &.input--small { width: 10em; } - &.input--medium { width: 15em; } - &.input--large { width: 20em; } -} - -.input--prepend { position: relative; } -.input--prepend .prepend { - position: absolute; - left: 7px; - top: 16px; - font-weight: bold; - line-height: 0; - font-size: 13px; - color: rgba($grey, 0.8); - } -.input--prepend input { padding-left: 24px; } - - -.field input, -.field textarea { - margin-bottom: 0; -} - -input.date-picker { - max-width: 100px; -} - -.input--percent { - position: relative; -} -.input--percent input { - width: 58px; - text-align: right; - padding-right: 18px; -} -.input--percent:after { - position: absolute; - left: 40px; - font-size: 12px; - color: grey; - top: 4px; - content: '%'; -} - -select { - width: 100%; - height: 33px; - background: white; - line-height: 1.5; - &.selectState { - width: 70px; - } -} - -select.select { - -webkit-appearance: none; - -moz-appearance: none; - -ms-appearance: none; - -o-appearance: none; - appearance: none; - background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAmCAYAAADTGStiAAAACXBIWXMAAAsTAAALEwEAmpwYAAA51WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU0OTExLCAyMDEzLzEwLzI5LTExOjQ3OjE2ICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMTUtMDQtMTZUMTY6MDI6MTEtMDc6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNS0wNC0xNlQxNjoyODo1My0wNzowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTUtMDQtMTZUMTY6Mjg6NTMtMDc6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6NTUzOGQwOWUtNzgzZS00YTU4LTgzN2YtZTEwNTNkYmRjYWY3PC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjYyODI5YmE4LWVlMTUtNDlkYi04NmUwLTZjMTc0ZGUyOWE3ZTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjYyODI5YmE4LWVlMTUtNDlkYi04NmUwLTZjMTc0ZGUyOWE3ZTwveG1wTU06T3JpZ2luYWxEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SGlzdG9yeT4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmNyZWF0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo2MjgyOWJhOC1lZTE1LTQ5ZGItODZlMC02YzE3NGRlMjlhN2U8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMTZUMTY6MDI6MTEtMDc6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6NTUzOGQwOWUtNzgzZS00YTU4LTgzN2YtZTEwNTNkYmRjYWY3PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTE2VDE2OjI4OjUzLTA3OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpTZXE+CiAgICAgICAgIDwveG1wTU06SGlzdG9yeT4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzIwMDAwLzEwMDAwPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+NjU1MzU8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjMwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjM4PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz79B9nqAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAACySURBVHja7NYxCsJAEAXQ+WoVLT2FvScJpMkQrZNLJRAbwZPY7y1SJp18KxGFQFYMWvxpd+Ex/GEYkLRf1MJ+VIIFCxYsWPD/waupH9vTpQZwGHsn2XieHr/e8dB3Jckwgoah78qYjjH1AgFgbXve0ZZXwJInagPstnfPQsw1E5WxexYIq166hVXuWZh9uIo8rUk2j1yLPK1nHa73vNebbRKb60cZa4EIFixYsGDBs9V9AIovRFol6phnAAAAAElFTkSuQmCC"); - background-position: center right; - background-repeat: no-repeat; - padding-right: 1rem; -} - -label { - @include no-select; - color: $sea-foam; - display: inherit; - font-weight: bold; - text-align: left; - margin-bottom: 5px; -} - -input { - width: 100%; -} - -textarea { - border-color: lighten($grey, 35%); - border-bottom: 2px solid lighten($grey, 35%); - @include transition(border-bottom 0.2s ease); - color: $charcoal; - padding: 5px 8px; - font-size: 15px; - line-height: 1.3; - resize: none; - width: 100%; - &:focus { - border-bottom: 2px solid lighten($grey, 15%); - } -} - -input[type="submit"] { - @include border-radius(0); -} - -input[type="file"] { - height: auto; - line-height: 1; - border: none; - &.disabled { - pointer-events: none; - @include opacity(0.5); - } -} - -input[type="file"]::-webkit-file-upload-button { - padding: 7px; - @include setBackgroundAndHover($sky); - color: white; - font-weight: bold; - @include border-radius(3px); - cursor: pointer; - border: none; - font-weight: bold; - &:focus {outline:none;} -} - -.input--mini { - width: 3em; -} -.input--small { - width: 10em; -} -.input--medium { - width: 15em; -} -.input--large { - width: 20em; -} - -input[type='text'].input--bigText { - font-size: 20px; -} - -input[readonly] { - @include transition(none); - @include no-select; - pointer-events: none; - padding: 0 5px; - font-size: 15px; - line-height: 26px; -} -textarea[readonly] { - @include transition(none); - border-color: rgba(black, 0.02); -} - -input.removeField { - border-color: rgba($red, 0.1); - @include transition(none); - @include no-select; - pointer-events: none; -} - - - -input::-ms-clear { display: none; } - -// Radio buttons -input[type='radio'] { - display: none; -} -input[type='radio'] + label { - @include clickableLabel; - margin-bottom: 5px; -} -input[type='radio'] + label:before { - content: ''; - margin-right: 8px; - background-color: #fff; - vertical-align: middle; - display: inline-block; - width: 17px; - height: 17px; - border: 1px #ccc solid; - @include border-radius(50%); -} -input[type='radio'].radio--large + label:before { - width: 21px; - height: 21px; -} -input[type='radio'] + label:hover:before { - background-color: rgba(black, 0.1); -} -input[type='radio'] + label { - cursor: pointer; - position: relative; -} -input[type='radio']:checked + label:before { - background-color: $bluegrass; -} -input[type='radio'].radio--both { - @extend input[type='radio']; - & + label { - @include opacity(0.9); - } - &:checked + label:before { - background-color: rgba($grey, 0.8); - } -} - -// Button for clearing out input fields -.clear-input { - position: absolute; - right: 6px; - cursor: pointer; - top: 6px; - font-size: 18px; - @include setColorAndHover(rgba($grey,0.4)); -} - -// Checkbox -fieldset.checkbox input, -input[type='checkbox'] { - display: none; -} -fieldset.checkbox label, -input[type='checkbox'] + label { - @include clickableLabel; -} -fieldset.checkbox label:before, -input[type='checkbox'] + label:before { - content: ''; - vertical-align: text-bottom; - display: inline-block; - background: $fog; - border: 1px #ccc solid; - margin-right: 5px; - float: none; - width: 20px; - height: 20px; - line-height: 20px; - padding: 0; - text-align: center; - font-family: 'FontAwesome'; - color: $sea-foam; - font-size: 15px; -} -fieldset.checkbox label:hover:before, -input[type='checkbox']:checked + label:before { - content: '\f00c'; -} -fieldset.checkbox label:hover:before, -input[type='checkbox'] + label:hover:before { - background-color: $sage; -} -.prepend--dollar { // add this class to the parent of the input - @extend .u-prepend; - @include basicInputs { padding-left: 18px;} - &:before { - padding: 0 8px; - content: '$'; - font-size: 15px; - line-height: 29px; - } -} - -.prepend--euro { // add this class to the parent of the input - @extend .u-prepend; - @include basicInputs { padding-left: 18px;} - &:before { - padding: 0 8px; - content: '€'; - font-size: 15px; - line-height: 29px; - } -} - -@media screen and (max-width: 350px) { - @include basicInputs { - font-size: 13px; - } -} diff --git a/app/assets/stylesheets/components/inputs.scss b/app/assets/stylesheets/components/inputs.scss new file mode 100644 index 000000000..09571f288 --- /dev/null +++ b/app/assets/stylesheets/components/inputs.scss @@ -0,0 +1,349 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +@mixin basicInputs { + input[type="text"], input, input[type="email"], input[type="number"], input[type="password"], input[type="tel"], select { + @content; + } +} + +@mixin inputStyleReset { + color: $charcoal; + padding: 4px 8px; + font-size: 16px; // inputs should all be 16px to prevent zooming on mobile + @include border-radius(0); + @include transition(border-color 0.2s ease-out); + border-top: 1px solid lighten($grey, 35%); + border-right: 1px solid lighten($grey, 35%); + border-left: 1px solid lighten($grey, 35%); + border-bottom: 2px solid lighten($grey, 35%); + margin-bottom: 12px; + &:focus { + border-bottom: 2px solid lighten($grey, 15%); + } +} + +@mixin clickableLabel { + cursor: pointer; + display: inline-block; + position: relative; + font-weight: normal; + @include setColorAndHover($charcoal); + margin: 0; +} + + +@include basicInputs { + @include inputStyleReset; + &.input--50 { max-width: 50px; } + &.input--100 { max-width: 100px; } + &.input--150 { max-width: 150px; } + &.input--175 { max-width: 175px; } + &.input--200 { max-width: 200px; } + &.input--250 { max-width: 250px; } + &.input--300 { max-width: 300px; } + &.input--400 { max-width: 400px; } + &.input--half { max-width: 48%; } + &.input--mini { width: 3em; } + &.input--small { width: 10em; } + &.input--medium { width: 15em; } + &.input--large { width: 20em; } +} + +.input--prepend { position: relative; } +.input--prepend .prepend { + position: absolute; + left: 7px; + top: 16px; + font-weight: bold; + line-height: 0; + font-size: 13px; + color: rgba($grey, 0.8); + } +.input--prepend input { padding-left: 24px; } + + +.field input, +.field textarea { + margin-bottom: 0; +} + +input.date-picker { + max-width: 100px; +} + +.input--percent { + position: relative; +} +.input--percent input { + width: 58px; + text-align: right; + padding-right: 18px; +} +.input--percent:after { + position: absolute; + left: 40px; + font-size: 12px; + color: grey; + top: 4px; + content: '%'; +} + +select { + width: 100%; + height: 33px; + background: white; + line-height: 1.5; + &.selectState { + width: 70px; + } +} + +select.select { + -webkit-appearance: none; + -moz-appearance: none; + -ms-appearance: none; + -o-appearance: none; + appearance: none; + background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAmCAYAAADTGStiAAAACXBIWXMAAAsTAAALEwEAmpwYAAA51WlUWHRYTUw6Y29tLmFkb2JlLnhtcAAAAAAAPD94cGFja2V0IGJlZ2luPSLvu78iIGlkPSJXNU0wTXBDZWhpSHpyZVN6TlRjemtjOWQiPz4KPHg6eG1wbWV0YSB4bWxuczp4PSJhZG9iZTpuczptZXRhLyIgeDp4bXB0az0iQWRvYmUgWE1QIENvcmUgNS41LWMwMjEgNzkuMTU0OTExLCAyMDEzLzEwLzI5LTExOjQ3OjE2ICAgICAgICAiPgogICA8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAgICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyIKICAgICAgICAgICAgeG1sbnM6cGhvdG9zaG9wPSJodHRwOi8vbnMuYWRvYmUuY29tL3Bob3Rvc2hvcC8xLjAvIgogICAgICAgICAgICB4bWxuczp4bXBNTT0iaHR0cDovL25zLmFkb2JlLmNvbS94YXAvMS4wL21tLyIKICAgICAgICAgICAgeG1sbnM6c3RFdnQ9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9zVHlwZS9SZXNvdXJjZUV2ZW50IyIKICAgICAgICAgICAgeG1sbnM6dGlmZj0iaHR0cDovL25zLmFkb2JlLmNvbS90aWZmLzEuMC8iCiAgICAgICAgICAgIHhtbG5zOmV4aWY9Imh0dHA6Ly9ucy5hZG9iZS5jb20vZXhpZi8xLjAvIj4KICAgICAgICAgPHhtcDpDcmVhdG9yVG9vbD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3htcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhtcDpDcmVhdGVEYXRlPjIwMTUtMDQtMTZUMTY6MDI6MTEtMDc6MDA8L3htcDpDcmVhdGVEYXRlPgogICAgICAgICA8eG1wOk1vZGlmeURhdGU+MjAxNS0wNC0xNlQxNjoyODo1My0wNzowMDwveG1wOk1vZGlmeURhdGU+CiAgICAgICAgIDx4bXA6TWV0YWRhdGFEYXRlPjIwMTUtMDQtMTZUMTY6Mjg6NTMtMDc6MDA8L3htcDpNZXRhZGF0YURhdGU+CiAgICAgICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgICAgIDxwaG90b3Nob3A6Q29sb3JNb2RlPjM8L3Bob3Rvc2hvcDpDb2xvck1vZGU+CiAgICAgICAgIDx4bXBNTTpJbnN0YW5jZUlEPnhtcC5paWQ6NTUzOGQwOWUtNzgzZS00YTU4LTgzN2YtZTEwNTNkYmRjYWY3PC94bXBNTTpJbnN0YW5jZUlEPgogICAgICAgICA8eG1wTU06RG9jdW1lbnRJRD54bXAuZGlkOjYyODI5YmE4LWVlMTUtNDlkYi04NmUwLTZjMTc0ZGUyOWE3ZTwveG1wTU06RG9jdW1lbnRJRD4KICAgICAgICAgPHhtcE1NOk9yaWdpbmFsRG9jdW1lbnRJRD54bXAuZGlkOjYyODI5YmE4LWVlMTUtNDlkYi04NmUwLTZjMTc0ZGUyOWE3ZTwveG1wTU06T3JpZ2luYWxEb2N1bWVudElEPgogICAgICAgICA8eG1wTU06SGlzdG9yeT4KICAgICAgICAgICAgPHJkZjpTZXE+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPmNyZWF0ZWQ8L3N0RXZ0OmFjdGlvbj4KICAgICAgICAgICAgICAgICAgPHN0RXZ0Omluc3RhbmNlSUQ+eG1wLmlpZDo2MjgyOWJhOC1lZTE1LTQ5ZGItODZlMC02YzE3NGRlMjlhN2U8L3N0RXZ0Omluc3RhbmNlSUQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDp3aGVuPjIwMTUtMDQtMTZUMTY6MDI6MTEtMDc6MDA8L3N0RXZ0OndoZW4+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpzb2Z0d2FyZUFnZW50PkFkb2JlIFBob3Rvc2hvcCBDQyAoTWFjaW50b3NoKTwvc3RFdnQ6c29mdHdhcmVBZ2VudD4KICAgICAgICAgICAgICAgPC9yZGY6bGk+CiAgICAgICAgICAgICAgIDxyZGY6bGkgcmRmOnBhcnNlVHlwZT0iUmVzb3VyY2UiPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6YWN0aW9uPnNhdmVkPC9zdEV2dDphY3Rpb24+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDppbnN0YW5jZUlEPnhtcC5paWQ6NTUzOGQwOWUtNzgzZS00YTU4LTgzN2YtZTEwNTNkYmRjYWY3PC9zdEV2dDppbnN0YW5jZUlEPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6d2hlbj4yMDE1LTA0LTE2VDE2OjI4OjUzLTA3OjAwPC9zdEV2dDp3aGVuPgogICAgICAgICAgICAgICAgICA8c3RFdnQ6c29mdHdhcmVBZ2VudD5BZG9iZSBQaG90b3Nob3AgQ0MgKE1hY2ludG9zaCk8L3N0RXZ0OnNvZnR3YXJlQWdlbnQ+CiAgICAgICAgICAgICAgICAgIDxzdEV2dDpjaGFuZ2VkPi88L3N0RXZ0OmNoYW5nZWQ+CiAgICAgICAgICAgICAgIDwvcmRmOmxpPgogICAgICAgICAgICA8L3JkZjpTZXE+CiAgICAgICAgIDwveG1wTU06SGlzdG9yeT4KICAgICAgICAgPHRpZmY6T3JpZW50YXRpb24+MTwvdGlmZjpPcmllbnRhdGlvbj4KICAgICAgICAgPHRpZmY6WFJlc29sdXRpb24+NzIwMDAwLzEwMDAwPC90aWZmOlhSZXNvbHV0aW9uPgogICAgICAgICA8dGlmZjpZUmVzb2x1dGlvbj43MjAwMDAvMTAwMDA8L3RpZmY6WVJlc29sdXRpb24+CiAgICAgICAgIDx0aWZmOlJlc29sdXRpb25Vbml0PjI8L3RpZmY6UmVzb2x1dGlvblVuaXQ+CiAgICAgICAgIDxleGlmOkNvbG9yU3BhY2U+NjU1MzU8L2V4aWY6Q29sb3JTcGFjZT4KICAgICAgICAgPGV4aWY6UGl4ZWxYRGltZW5zaW9uPjMwPC9leGlmOlBpeGVsWERpbWVuc2lvbj4KICAgICAgICAgPGV4aWY6UGl4ZWxZRGltZW5zaW9uPjM4PC9leGlmOlBpeGVsWURpbWVuc2lvbj4KICAgICAgPC9yZGY6RGVzY3JpcHRpb24+CiAgIDwvcmRmOlJERj4KPC94OnhtcG1ldGE+CiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIAogICAgICAgICAgICAgICAgICAgICAgICAgICAgCjw/eHBhY2tldCBlbmQ9InciPz79B9nqAAAAIGNIUk0AAHolAACAgwAA+f8AAIDpAAB1MAAA6mAAADqYAAAXb5JfxUYAAACySURBVHja7NYxCsJAEAXQ+WoVLT2FvScJpMkQrZNLJRAbwZPY7y1SJp18KxGFQFYMWvxpd+Ex/GEYkLRf1MJ+VIIFCxYsWPD/waupH9vTpQZwGHsn2XieHr/e8dB3Jckwgoah78qYjjH1AgFgbXve0ZZXwJInagPstnfPQsw1E5WxexYIq166hVXuWZh9uIo8rUk2j1yLPK1nHa73vNebbRKb60cZa4EIFixYsGDBs9V9AIovRFol6phnAAAAAElFTkSuQmCC"); + background-position: center right; + background-repeat: no-repeat; + padding-right: 1rem; +} + +label { + @include no-select; + color: $sea-foam; + display: inherit; + font-weight: bold; + text-align: left; + margin-bottom: 5px; +} + +input { + width: 100%; +} + +textarea { + border-color: lighten($grey, 35%); + border-bottom: 2px solid lighten($grey, 35%); + @include transition(border-bottom 0.2s ease); + color: $charcoal; + padding: 5px 8px; + font-size: 15px; + line-height: 1.3; + resize: none; + width: 100%; + &:focus { + border-bottom: 2px solid lighten($grey, 15%); + } +} + +input[type="submit"] { + @include border-radius(0); +} + +input[type="file"] { + height: auto; + line-height: 1; + border: none; + &.disabled { + pointer-events: none; + @include opacity(0.5); + } +} + +input[type="file"]::-webkit-file-upload-button { + padding: 7px; + @include setBackgroundAndHover($sky); + color: white; + font-weight: bold; + @include border-radius(3px); + cursor: pointer; + border: none; + font-weight: bold; + &:focus {outline:none;} +} + +.input--mini { + width: 3em; +} +.input--small { + width: 10em; +} +.input--medium { + width: 15em; +} +.input--large { + width: 20em; +} + +input[type='text'].input--bigText { + font-size: 20px; +} + +input[readonly] { + @include transition(none); + @include no-select; + pointer-events: none; + padding: 0 5px; + font-size: 15px; + line-height: 26px; +} +textarea[readonly] { + @include transition(none); + border-color: rgba(black, 0.02); +} + +input.removeField { + border-color: rgba($red, 0.1); + @include transition(none); + @include no-select; + pointer-events: none; +} + + + +input::-ms-clear { display: none; } + +// Radio buttons +input[type='radio'] { + display: none; +} +input[type='radio'] + label { + @include clickableLabel; + margin-bottom: 5px; +} +input[type='radio'] + label:before { + content: ''; + margin-right: 8px; + background-color: #fff; + vertical-align: middle; + display: inline-block; + width: 17px; + height: 17px; + border: 1px #ccc solid; + @include border-radius(50%); +} +input[type='radio'].radio--large + label:before { + width: 21px; + height: 21px; +} +input[type='radio'] + label:hover:before { + background-color: rgba(black, 0.1); +} +input[type='radio'] + label { + cursor: pointer; + position: relative; +} +input[type='radio']:checked + label:before { + background-color: $bluegrass; +} +input[type='radio'].radio--both { + @extend input[type='radio']; + & + label { + @include opacity(0.9); + } + &:checked + label:before { + background-color: rgba($grey, 0.8); + } +} + +// Button for clearing out input fields +.clear-input { + position: absolute; + right: 6px; + cursor: pointer; + top: 6px; + font-size: 18px; + @include setColorAndHover(rgba($grey,0.4)); +} + +// Checkbox +fieldset.checkbox input, +input[type='checkbox'] { + display: none; +} +fieldset.checkbox label, +input[type='checkbox'] + label { + @include clickableLabel; +} +fieldset.checkbox label:before, +input[type='checkbox'] + label:before { + content: ''; + vertical-align: text-bottom; + display: inline-block; + background: $fog; + border: 1px #ccc solid; + margin-right: 5px; + float: none; + width: 20px; + height: 20px; + line-height: 20px; + padding: 0; + text-align: center; + font-family: 'FontAwesome'; + color: $sea-foam; + font-size: 15px; +} +fieldset.checkbox label:hover:before, +input[type='checkbox']:checked + label:before { + content: '\f00c'; +} +fieldset.checkbox label:hover:before, +input[type='checkbox'] + label:hover:before { + background-color: $sage; +} +.prepend--dollar { // add this class to the parent of the input + @extend .u-prepend; + @include basicInputs { padding-left: 18px;} + &:before { + padding: 0 8px; + content: '$'; + font-size: 15px; + line-height: 29px; + } +} + +.prepend--euro { // add this class to the parent of the input + @extend .u-prepend; + @include basicInputs { padding-left: 18px;} + &:before { + padding: 0 8px; + content: '€'; + font-size: 15px; + line-height: 29px; + } +} + +@media screen and (max-width: 350px) { + @include basicInputs { + font-size: 13px; + } +} +.StripeElement { + color: $charcoal; + padding: 4px 8px; + font-size: 16px; // inputs should all be 16px to prevent zooming on mobile + @include border-radius(0); + @include transition(border-color 0.2s ease-out); + border-top: 1px solid lighten($grey, 35%); + border-right: 1px solid lighten($grey, 35%); + border-left: 1px solid lighten($grey, 35%); + border-bottom: 2px solid lighten($grey, 35%); + margin-bottom: 12px; + background-color: white + } + + .StripeElement--focus { + border-bottom: 2px solid lighten($grey, 15%); + } + + .StripeElement--invalid { + border-color: #ff4f4f; + } + + .StripeElement--webkit-autofill { + background-color: #fefde5 !important; + } + diff --git a/app/assets/stylesheets/components/legend.css.scss b/app/assets/stylesheets/components/legend.scss similarity index 100% rename from app/assets/stylesheets/components/legend.css.scss rename to app/assets/stylesheets/components/legend.scss diff --git a/app/assets/stylesheets/components/loading.css.scss b/app/assets/stylesheets/components/loading.scss similarity index 100% rename from app/assets/stylesheets/components/loading.css.scss rename to app/assets/stylesheets/components/loading.scss diff --git a/app/assets/stylesheets/components/modals.css.scss b/app/assets/stylesheets/components/modals.scss similarity index 100% rename from app/assets/stylesheets/components/modals.css.scss rename to app/assets/stylesheets/components/modals.scss diff --git a/app/assets/stylesheets/components/nonprofit_bank_accounts.css.scss b/app/assets/stylesheets/components/nonprofit_bank_accounts.scss similarity index 100% rename from app/assets/stylesheets/components/nonprofit_bank_accounts.css.scss rename to app/assets/stylesheets/components/nonprofit_bank_accounts.scss diff --git a/app/assets/stylesheets/components/notification_alerts.css.scss b/app/assets/stylesheets/components/notification_alerts.scss similarity index 100% rename from app/assets/stylesheets/components/notification_alerts.css.scss rename to app/assets/stylesheets/components/notification_alerts.scss diff --git a/app/assets/stylesheets/components/npo_card.css.scss b/app/assets/stylesheets/components/npo_card.scss similarity index 100% rename from app/assets/stylesheets/components/npo_card.css.scss rename to app/assets/stylesheets/components/npo_card.scss diff --git a/app/assets/stylesheets/components/page_tabs.css.scss b/app/assets/stylesheets/components/page_tabs.scss similarity index 100% rename from app/assets/stylesheets/components/page_tabs.css.scss rename to app/assets/stylesheets/components/page_tabs.scss diff --git a/app/assets/stylesheets/components/pagination.css.scss b/app/assets/stylesheets/components/pagination.scss similarity index 100% rename from app/assets/stylesheets/components/pagination.css.scss rename to app/assets/stylesheets/components/pagination.scss diff --git a/app/assets/stylesheets/components/panels_layout.css.scss b/app/assets/stylesheets/components/panels_layout.scss similarity index 100% rename from app/assets/stylesheets/components/panels_layout.css.scss rename to app/assets/stylesheets/components/panels_layout.scss diff --git a/app/assets/stylesheets/components/parsley.css.scss b/app/assets/stylesheets/components/parsley.scss similarity index 100% rename from app/assets/stylesheets/components/parsley.css.scss rename to app/assets/stylesheets/components/parsley.scss diff --git a/app/assets/stylesheets/components/pastel_boxes.css.scss b/app/assets/stylesheets/components/pastel_boxes.css.scss deleted file mode 100644 index d8e072d14..000000000 --- a/app/assets/stylesheets/components/pastel_boxes.css.scss +++ /dev/null @@ -1,59 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -@mixin pastelBox { - border: 1px solid rgba(black, 0.03); - header { - text-align: center; - background: rgba(black, 0.04); - font-size: 20px; - font-weight: bold; - padding: 10px 15px; - } - header i { - font-size: 18px; - margin-right: 5px; - @include opacity(0.7); - } - footer { - padding: 10px 15px; - text-align: center; - border-top: 1px solid rgba(black, 0.03); - background: rgba(black, 0.01); - } -} -.pastelBox-body { - padding: 15px; -} -.pastelBox--green { - @include pastelBox; - background: rgb(242, 249, 241); -} // used for donation related -.pastelBox--orange { - @include pastelBox; - background: rgb(255, 247, 243); -} // used for donor related -.pastelBox--blue { - @include pastelBox; - background: rgb(241, 252, 252); -} // used for recurring related -.pastelBox--white{ - @include pastelBox; - background: rgba(white, 0.9); -} -.pastelBox--grey--dark{ - @include pastelBox; - background: darken($fog, 2); -} -.pastelBox--grey{ - @include pastelBox; - background: $fog; -} -.pastelBox--yellow { - @include pastelBox; - background: rgb(254, 251, 233); -} // used for events and campaigns related -.pastelBox--looseleaf { - @include pastelBox; - background: $looseleaf; -} diff --git a/app/assets/stylesheets/components/pastel_boxes.scss b/app/assets/stylesheets/components/pastel_boxes.scss new file mode 100644 index 000000000..c604a0166 --- /dev/null +++ b/app/assets/stylesheets/components/pastel_boxes.scss @@ -0,0 +1,63 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +@mixin pastelBox { + border: 1px solid rgba(black, 0.03); + header { + text-align: center; + background: rgba(black, 0.04); + font-size: 20px; + font-weight: bold; + padding: 10px 15px; + } + header i { + font-size: 18px; + margin-right: 5px; + @include opacity(0.7); + } + footer { + padding: 10px 15px; + text-align: center; + border-top: 1px solid rgba(black, 0.03); + background: rgba(black, 0.01); + } +} +.pastelBox-body { + padding: 15px; +} +.pastelBox--green { + @include pastelBox; + background: rgb(242, 249, 241); +} // used for donation related +.pastelBox--orange { + @include pastelBox; + background: #FFF7F3; +} // used for donor related +.pastelBox--blue { + @include pastelBox; + background: rgb(241, 252, 252); +} // used for recurring related +.pastelBox--white{ + @include pastelBox; + background: rgba(white, 0.9); +} +.pastelBox--grey--dark{ + @include pastelBox; + background: darken($fog, 2); +} +.pastelBox--grey{ + @include pastelBox; + background: $fog; +} +.pastelBox--yellow { + @include pastelBox; + background: rgb(254, 251, 233); +} // used for events and campaigns related +.pastelBox--looseleaf { + @include pastelBox; + background: $looseleaf; +} +.pastelBox--red { + @include pastelBox; + background: #FFE0E0 +} diff --git a/app/assets/stylesheets/components/press_row.css.scss b/app/assets/stylesheets/components/press_row.css.scss deleted file mode 100644 index 2ec9410d3..000000000 --- a/app/assets/stylesheets/components/press_row.css.scss +++ /dev/null @@ -1,13 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -.press-row { - background: rgb(230,230,230); - padding: 20px 0; -} - -.press-image { - float: left; - width: 20%; - padding: 0 3%; - @include opacity(0.3); -} - diff --git a/app/assets/stylesheets/components/progress_bar.css.scss b/app/assets/stylesheets/components/progress_bar.css.scss deleted file mode 100644 index 41f618fff..000000000 --- a/app/assets/stylesheets/components/progress_bar.css.scss +++ /dev/null @@ -1,150 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -@mixin progressBar { - width: 100%; - overflow: hidden; - background-color: white; - position: relative; - @include box-shadow(0 0 1px 0 rgba($grey, 0.5)); - .goalAmount, - .totalRaised { - position: absolute; - line-height: 1; - color: rgba(black, 0.7); - } - .dollar { - color: rgba(black, 0.6); - } -} -@mixin progressBar-fill($height) { - width: 0%; - height: $height; - @include transition(width 1s ease-out); - max-width: 100%; - background-color: rgba($bluegrass, 0.5); -} // the height of the fill dictates the height of the progressBar - - -// progressBar--large is used on campaigns -.progressBar--large { - @include border-radius(20px); - @include progressBar; - border: 3px white solid; -} -.progressBar--large-fill { - @include progressBar-fill(26px); // passing in the height - @include border-radius(20px 0 0 20px); -} -.progressBar--large .goalAmount { - font-size: 20px; - right: 9px; - top: 4px; -} -.progressBar--large .goalAmount .dollar { - font-size: 14px; - margin-right: -4px; - line-height: 1.2; -} - -.progressBar { - @include progressBar -} - -// progressBar--medium is used on dasboard and todo list -.progressBar--medium { - @include border-radius(10px); - @include progressBar; - font-size: 14px; - border: 2px white solid; -} -.progressBar--medium .dollar { - font-size: 11px; - margin-right: -3.5px; - line-height: 1.1; -} -.progressBar--medium .goalAmount, -.progressBar--medium .totalRaised { - top: 1.5px; -} -.progressBar--medium .goalAmount { - right: 7px; -} -.progressBar--medium .totalRaised { - left: 7px; -} -.progressBar--medium-fill { - @include progressBar-fill(18px); // passing in the height - @include border-radius(10px 0 0 10px); -} - - -// progressBar--small is used on campaign cards -.progressBar--small { - @include border-radius(8px); - @include progressBar; -} -.progressBar--small-fill { - @include progressBar-fill(10px); // passing in the height - @include border-radius(6px 0 0 6px); -} - - -// progressBar--app lives in the footer and indicates a loading state -// called like this: appl.def('loading', true) -.progressBar--app { - @include progressBar; - padding: 0; - position: fixed; - bottom: 0; - left: 0; -} -.progressBar--app .progressBar-fill--striped { - height: 15px; - width: 100%; -} -// these keyframes animate the diagonal stripes -@-webkit-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -@-moz-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -@-ms-keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -@-o-keyframes progress-bar-stripes{ - from{background-position:0 0} - to{background-position:40px 0} -} -@keyframes progress-bar-stripes{ - from{background-position:40px 0} - to{background-position:0 0} -} -.progressBar-fill--striped { - width: 0%; - height: 15px; - -webkit-animation: progress-bar-stripes 2s linear infinite; - -moz-animation: progress-bar-stripes 2s linear infinite; - -ms-animation: progress-bar-stripes 2s linear infinite; - -o-animation: progress-bar-stripes 2s linear infinite; - animation: progress-bar-stripes 2s linear infinite; - background-color: $bluegrass; - background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(white,0.3)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(white,0.3)), color-stop(0.75, rgba(white,0.3)), color-stop(0.75, transparent), to(transparent)); - background-image: -webkit-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: -moz-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: -o-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - background-image: linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); - -webkit-background-size: 40px 40px; - -moz-background-size: 40px 40px; - -o-background-size: 40px 40px; - background-size: 40px 40px; - background-repeat: repeat-x; - -webkit-transition: width 5s ease; - -moz-transition: width 5s ease; - -o-transition: width 5s ease; - transition: width 5s ease; -} diff --git a/app/assets/stylesheets/components/progress_bar.scss b/app/assets/stylesheets/components/progress_bar.scss new file mode 100644 index 000000000..f379f52bf --- /dev/null +++ b/app/assets/stylesheets/components/progress_bar.scss @@ -0,0 +1,134 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +@mixin progressBar { + width: 100%; + overflow: hidden; + background-color: white; + position: relative; + @include box-shadow(0 0 1px 0 rgba($grey, 0.5)); + .goalAmount, + .totalRaised { + position: absolute; + line-height: 1; + color: rgba(black, 0.7); + } + .dollar { + color: rgba(black, 0.6); + } +} +@mixin progressBar-fill($height) { + width: 0%; + height: $height; + @include transition(width 1s ease-out); + max-width: 100%; + background-color: rgba($bluegrass, 0.5); +} // the height of the fill dictates the height of the progressBar + + +// progressBar--large is used on campaigns +.progressBar--large { + @include border-radius(20px); + @include progressBar; + border: 3px white solid; +} +.progressBar--large-fill { + @include progressBar-fill(26px); // passing in the height + @include border-radius(20px 0 0 20px); +} +.progressBar--large .goalAmount { + font-size: 20px; + right: 9px; + top: 4px; +} +.progressBar--large .goalAmount .dollar { + font-size: 14px; + margin-right: -4px; + line-height: 1.2; +} + +.progressBar { + @include progressBar +} + +// progressBar--medium is used on dasboard and todo list +.progressBar--medium { + @include border-radius(10px); + @include progressBar; + font-size: 14px; + border: 2px white solid; +} +.progressBar--medium .dollar { + font-size: 11px; + margin-right: -3.5px; + line-height: 1.1; +} +.progressBar--medium .goalAmount, +.progressBar--medium .totalRaised { + top: 1.5px; +} +.progressBar--medium .goalAmount { + right: 7px; +} +.progressBar--medium .totalRaised { + left: 7px; +} +.progressBar--medium-fill { + @include progressBar-fill(18px); // passing in the height + @include border-radius(10px 0 0 10px); +} + + +// progressBar--small is used on campaign cards +.progressBar--small { + @include border-radius(8px); + @include progressBar; +} +.progressBar--small-fill { + @include progressBar-fill(10px); // passing in the height + @include border-radius(6px 0 0 6px); +} + + +// progressBar--app lives in the footer and indicates a loading state +// called like this: appl.def('loading', true) +.progressBar--app { + @include progressBar; + padding: 0; + position: fixed; + bottom: 0; + left: 0; +} +.progressBar--app .progressBar-fill--striped { + height: 15px; + width: 100%; +} +// these keyframes animate the diagonal stripes +@keyframes progress-bar-stripes{ + from{background-position:40px 0} + to{background-position:0 0} +} +.progressBar-fill--striped { + width: 0%; + height: 15px; + -webkit-animation: progress-bar-stripes 2s linear infinite; + -moz-animation: progress-bar-stripes 2s linear infinite; + -ms-animation: progress-bar-stripes 2s linear infinite; + -o-animation: progress-bar-stripes 2s linear infinite; + animation: progress-bar-stripes 2s linear infinite; + background-color: $bluegrass; + background-image: -webkit-gradient(linear, 0 100%, 100% 0, color-stop(0.25, rgba(white,0.3)), color-stop(0.25, transparent), color-stop(0.5, transparent), color-stop(0.5, rgba(white,0.3)), color-stop(0.75, rgba(white,0.3)), color-stop(0.75, transparent), to(transparent)); + background-image: -webkit-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: -moz-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: -o-linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + background-image: linear-gradient(45deg, rgba(white,0.3) 25%, transparent 25%, transparent 50%, rgba(white,0.3) 50%, rgba(white,0.3) 75%, transparent 75%, transparent); + -webkit-background-size: 40px 40px; + -moz-background-size: 40px 40px; + -o-background-size: 40px 40px; + background-size: 40px 40px; + background-repeat: repeat-x; + -webkit-transition: width 5s ease; + -moz-transition: width 5s ease; + -o-transition: width 5s ease; + transition: width 5s ease; +} diff --git a/app/assets/stylesheets/components/q_and_a.css.scss b/app/assets/stylesheets/components/q_and_a.scss similarity index 100% rename from app/assets/stylesheets/components/q_and_a.css.scss rename to app/assets/stylesheets/components/q_and_a.scss diff --git a/app/assets/stylesheets/components/side_nav.css.scss b/app/assets/stylesheets/components/side_nav.css.scss deleted file mode 100644 index 6ce4d1c6f..000000000 --- a/app/assets/stylesheets/components/side_nav.css.scss +++ /dev/null @@ -1,160 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'components/side_nav_dimensions'; - -$sideNav-spacing: 8px 15px; - - -.sideNav { - background: $charcoal; - height: 100%; - left: 0; - top: 0; - position: fixed; - @include box-shadow(0 0 10px 0 rgba(black, 0.5)); - overflow-x: hidden; - width: $sideNav-closed-width; - @include transition(width 0.2s ease-out); -} -.sideNav * { - @include no-select; -} -.sideNav-commitchangeLogo { - white-space: nowrap; - padding: 9px 14px; - display: block; - @include logo(18px, white, true, true); - @include opacity(1); - @include transition(opacity 0.2s ease-out); -} -.sideNav-commitchangeLogo img { - max-width: none; -} -.sideNav-commitchangeLogo:hover { - @include opacity(0.8); -} -.sideNav-commitchangeLogo .commitchangeLogo-text { - margin-left: 15px; -} -.sideNav-section { - padding: 6px 0; - border-bottom: 1px solid rgba(white, 0.15); -} -.sideNav-link { - position: relative; - color: white; - font-weight: bold; - cursor: pointer; - display: table; - table-layout: fixed; - @include transform(translateZ(0)); - @include transition(color 0.2s ease-out); -} -.sideNav-link:hover { - color: $logo-blue; -} -.sideNav-icon { - display: table-cell; - text-align: center; - font-size: 26px; - padding: $sideNav-spacing; -} -.sideNav-text { - @include ellipsis; - max-width: 165px; - font-size: 16px; - position: absolute; - top: 12px; - left: 62px; - height: 100%; - @include transform(translateZ(0)); - @include opacity(0); - @include transition(opacity 0.2s ease-out); -} -.sideNav-profile { - width: 27px; - @include border-radius(50%); - margin: $sideNav-spacing; - display: table-cell; -} -.sideNav-scrim { - @include transition(opacity 0.2s ease-out); - @include opacity(0); - visibility: hidden; - top: 0; - left: 0; - width: 100%; - height: 100%; - position: fixed; - background-color: rgba(black, 0.6); -} -.sideNav-scrim.is-showing { - visibility: visible; - @include opacity(1); -} -.sideNav-toggle { - @include transition(all 0.2s ease-out); - position: fixed; - display: none; - top: 0; - left: 0; - width: 36px; - height: 36px; - background: rgba(black, 0.6); - @include box-shadow(0 0 5px 0 rgba(black, 0.2)); - font-size: 20px; - color: white; - cursor: pointer; -} -.sideNav-toggle i { - @include no-select; - display: table-cell; - vertical-align: middle; - text-align: center; -} -.sideNav-toggle .fa-times { - display: none; -} -.sideNav-toggle.is-togglingOpen{ - background: $trans; - @include box-shadow(none); - .fa-times { - display: table-cell; - } - .fa-bars { - display: none; - } -} -@mixin open_side_nav { - width: $sideNav-opened-width; - .sideNav-text { - @include opacity(1); - } -} -@mixin close_side_nav { - width: 0; - padding-top: 34px; -} -.sideNav.is-open, -.sideNav.is-hamburgerStyle.is-open { - @include open_side_nav; -} -.sideNav.is-hamburgerStyle { - @include close_side_nav; -} -.sideNav-toggle.is-hamburgerStyle { - display: table; -} -@media screen and (min-width: 701px) { - .sideNav:hover { - @include open_side_nav; - } -} -@media screen and (max-width: 700px) { - .sideNav { - @include close_side_nav; - } - .sideNav-toggle { - display: table; - } -} diff --git a/app/assets/stylesheets/components/side_nav.scss b/app/assets/stylesheets/components/side_nav.scss new file mode 100644 index 000000000..afbd3c1ff --- /dev/null +++ b/app/assets/stylesheets/components/side_nav.scss @@ -0,0 +1,159 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'components/side_nav_dimensions'; + +$sideNav-spacing: 8px 15px; + + +.sideNav { + background: $charcoal; + height: 100%; + left: 0; + top: 0; + position: fixed; + @include box-shadow(0 0 10px 0 rgba(black, 0.5)); + overflow-x: hidden; + width: $sideNav-closed-width; + @include transition(width 0.2s ease-out); +} +.sideNav * { + @include no-select; +} +.sideNav-commitchangeLogo { + white-space: nowrap; + padding: 9px 14px; + display: block; + @include logo(18px, white, true, true); + @include opacity(1); + @include transition(opacity 0.2s ease-out); +} +.sideNav-commitchangeLogo img { + max-width: none; +} +.sideNav-commitchangeLogo:hover { + @include opacity(0.8); +} +.sideNav-commitchangeLogo .commitchangeLogo-text { + margin-left: 15px; +} +.sideNav-section { + padding: 6px 0; + border-bottom: 1px solid rgba(white, 0.15); +} +.sideNav-link { + position: relative; + color: white; + font-weight: bold; + cursor: pointer; + display: block; + @include transform(translateZ(0)); + @include transition(color 0.2s ease-out); +} +.sideNav-link:hover { + color: $logo-blue; +} +.sideNav-icon { + display: table-cell; + text-align: center; + font-size: 26px; + padding: $sideNav-spacing; +} +.sideNav-text { + @include ellipsis; + max-width: 165px; + font-size: 16px; + position: absolute; + top: 12px; + left: 62px; + height: 100%; + @include transform(translateZ(0)); + @include opacity(0); + @include transition(opacity 0.2s ease-out); +} +.sideNav-profile { + width: 27px; + @include border-radius(50%); + margin: $sideNav-spacing; + display: table-cell; +} +.sideNav-scrim { + @include transition(opacity 0.2s ease-out); + @include opacity(0); + visibility: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; + position: fixed; + background-color: rgba(black, 0.6); +} +.sideNav-scrim.is-showing { + visibility: visible; + @include opacity(1); +} +.sideNav-toggle { + @include transition(all 0.2s ease-out); + position: fixed; + display: none; + top: 0; + left: 0; + width: 36px; + height: 36px; + background: rgba(black, 0.6); + @include box-shadow(0 0 5px 0 rgba(black, 0.2)); + font-size: 20px; + color: white; + cursor: pointer; +} +.sideNav-toggle i { + @include no-select; + display: table-cell; + vertical-align: middle; + text-align: center; +} +.sideNav-toggle .fa-times { + display: none; +} +.sideNav-toggle.is-togglingOpen{ + background: $trans; + @include box-shadow(none); + .fa-times { + display: table-cell; + } + .fa-bars { + display: none; + } +} +@mixin open_side_nav { + width: $sideNav-opened-width; + .sideNav-text { + @include opacity(1); + } +} +@mixin close_side_nav { + width: 0; + padding-top: 34px; +} +.sideNav.is-open, +.sideNav.is-hamburgerStyle.is-open { + @include open_side_nav; +} +.sideNav.is-hamburgerStyle { + @include close_side_nav; +} +.sideNav-toggle.is-hamburgerStyle { + display: table; +} +@media screen and (min-width: 701px) { + .sideNav:hover { + @include open_side_nav; + } +} +@media screen and (max-width: 700px) { + .sideNav { + @include close_side_nav; + } + .sideNav-toggle { + display: table; + } +} diff --git a/app/assets/stylesheets/components/side_nav_dimensions.css.scss b/app/assets/stylesheets/components/side_nav_dimensions.scss similarity index 100% rename from app/assets/stylesheets/components/side_nav_dimensions.css.scss rename to app/assets/stylesheets/components/side_nav_dimensions.scss diff --git a/app/assets/stylesheets/components/simple_tabs.css.scss b/app/assets/stylesheets/components/simple_tabs.scss similarity index 100% rename from app/assets/stylesheets/components/simple_tabs.css.scss rename to app/assets/stylesheets/components/simple_tabs.scss diff --git a/app/assets/stylesheets/components/steps_menu.css.scss b/app/assets/stylesheets/components/steps_menu.scss similarity index 100% rename from app/assets/stylesheets/components/steps_menu.css.scss rename to app/assets/stylesheets/components/steps_menu.scss diff --git a/app/assets/stylesheets/components/tables.css.scss b/app/assets/stylesheets/components/tables.scss similarity index 100% rename from app/assets/stylesheets/components/tables.css.scss rename to app/assets/stylesheets/components/tables.scss diff --git a/app/assets/stylesheets/components/tables/filtering/meta_status.css.scss b/app/assets/stylesheets/components/tables/filtering/meta_status.scss similarity index 100% rename from app/assets/stylesheets/components/tables/filtering/meta_status.css.scss rename to app/assets/stylesheets/components/tables/filtering/meta_status.scss diff --git a/app/assets/stylesheets/components/tags.css.scss b/app/assets/stylesheets/components/tags.scss similarity index 100% rename from app/assets/stylesheets/components/tags.css.scss rename to app/assets/stylesheets/components/tags.scss diff --git a/app/assets/stylesheets/components/team_card.css.scss b/app/assets/stylesheets/components/team_card.scss similarity index 100% rename from app/assets/stylesheets/components/team_card.css.scss rename to app/assets/stylesheets/components/team_card.scss diff --git a/app/assets/stylesheets/components/ticket_button.css.scss b/app/assets/stylesheets/components/ticket_button.css.scss deleted file mode 100644 index ce003003e..000000000 --- a/app/assets/stylesheets/components/ticket_button.css.scss +++ /dev/null @@ -1,83 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -.getTickets { - width: 100%; - display: inline-block; - position: relative; - text-align: center; - cursor: pointer; -} -.not-branded .getTickets { - @include transition(background-color 0.2s ease-out); - @include setBackgroundAndHover($bluegrass); -} - -.is-branded .getTickets { - @include opacity(0.7); - @include transition(opacity, 0.1s, ease-out); - &:hover { - @include opacity(0.9); - } -} - -// Ticket perforations at top and bottom -.getTickets:before, -.getTickets:after { - content: ''; - position: absolute; - background-size: contain; - height: 100%; - width: 4px; - top: 0; -} -.getTickets:before { - background-image: url(/assets/graphics/half-circle-right.svg); - left: 0; -} -.getTickets:after { - background-image: url(/assets/graphics/half-circle-left.svg); - right: 0; -} -.getTickets-text { - display: inline-block; - text-align: center; - font-size: 26px; - color: white; - @include open-sans; - font-weight: bold; - padding: 5px 0; - margin: 0; - line-height: 1; -} -.getTickets .doubleLines { - display: inline-block; - margin: 16px 0; -} - -.getTickets .doubleLines:before, -.getTickets .doubleLines:after { - width: 80%; - left: 10%; -} - -.not-branded .getTickets .doubleLines:before, -.not-branded .getTickets .doubleLines:after { - background: rgba(darken($bluegrass, 30%), 0.4); -} - -.is-branded .getTickets .doubleLines:before, -.is-branded .getTickets .doubleLines:after { - background: rgba(black, 0.15); -} - - -.cornerHoles--top { - position: relative; - display: block; - @include cornerHoles--top(1em, white); -} -.cornerHoles--bottom { - display: block; - @include cornerHoles--bottom(1em, white); -} \ No newline at end of file diff --git a/app/assets/stylesheets/components/ticket_button.scss.erb b/app/assets/stylesheets/components/ticket_button.scss.erb new file mode 100644 index 000000000..88f780320 --- /dev/null +++ b/app/assets/stylesheets/components/ticket_button.scss.erb @@ -0,0 +1,83 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +.getTickets { + width: 100%; + display: inline-block; + position: relative; + text-align: center; + cursor: pointer; +} +.not-branded .getTickets { + @include transition(background-color 0.2s ease-out); + @include setBackgroundAndHover($bluegrass); +} + +.is-branded .getTickets { + @include opacity(0.7); + @include transition(opacity, 0.1s, ease-out); + &:hover { + @include opacity(0.9); + } +} + +// Ticket perforations at top and bottom +.getTickets:before, +.getTickets:after { + content: ''; + position: absolute; + background-size: contain; + height: 100%; + width: 4px; + top: 0; +} +.getTickets:before { + background-image: url('<%= asset_path 'graphics/half-circle-right.svg' %>'); + left: 0; +} +.getTickets:after { + background-image: url('<%= asset_path 'graphics/half-circle-left.svg' %>'); + right: 0; +} +.getTickets-text { + display: inline-block; + text-align: center; + font-size: 26px; + color: white; + @include open-sans; + font-weight: bold; + padding: 5px 0; + margin: 0; + line-height: 1; +} +.getTickets .doubleLines { + display: inline-block; + margin: 16px 0; +} + +.getTickets .doubleLines:before, +.getTickets .doubleLines:after { + width: 80%; + left: 10%; +} + +.not-branded .getTickets .doubleLines:before, +.not-branded .getTickets .doubleLines:after { + background: rgba(darken($bluegrass, 30%), 0.4); +} + +.is-branded .getTickets .doubleLines:before, +.is-branded .getTickets .doubleLines:after { + background: rgba(black, 0.15); +} + + +.cornerHoles--top { + position: relative; + display: block; + @include cornerHoles--top(1em, white); +} +.cornerHoles--bottom { + display: block; + @include cornerHoles--bottom(1em, white); +} \ No newline at end of file diff --git a/app/assets/stylesheets/components/timeline.css.scss b/app/assets/stylesheets/components/timeline.scss similarity index 100% rename from app/assets/stylesheets/components/timeline.css.scss rename to app/assets/stylesheets/components/timeline.scss diff --git a/app/assets/stylesheets/components/todos.css.scss b/app/assets/stylesheets/components/todos.scss similarity index 100% rename from app/assets/stylesheets/components/todos.css.scss rename to app/assets/stylesheets/components/todos.scss diff --git a/app/assets/stylesheets/components/toggle_buttons.css.scss b/app/assets/stylesheets/components/toggle_buttons.scss similarity index 100% rename from app/assets/stylesheets/components/toggle_buttons.css.scss rename to app/assets/stylesheets/components/toggle_buttons.scss diff --git a/app/assets/stylesheets/components/toggle_q_a.css.scss b/app/assets/stylesheets/components/toggle_q_a.scss similarity index 100% rename from app/assets/stylesheets/components/toggle_q_a.css.scss rename to app/assets/stylesheets/components/toggle_q_a.scss diff --git a/app/assets/stylesheets/components/tooltips.css.scss b/app/assets/stylesheets/components/tooltips.scss similarity index 100% rename from app/assets/stylesheets/components/tooltips.css.scss rename to app/assets/stylesheets/components/tooltips.scss diff --git a/app/assets/stylesheets/components/top_nav.css.scss b/app/assets/stylesheets/components/top_nav.css.scss deleted file mode 100644 index a687283c4..000000000 --- a/app/assets/stylesheets/components/top_nav.css.scss +++ /dev/null @@ -1,37 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -.topNav { - background: rgba(0,0,0,0.04); - text-align: left; -} - -.topNav-logo { @include logo(18px, $charcoal--light, true, false); } - -.topNav .container { - display: table; - width: 100%; -} - -.topNav-horizontalLinks, -.topNav-logo { - display: table-cell; - vertical-align: middle; -} - -.topNav-horizontalLinks { text-align: right; } - -.topNav-horizontalLinks *, -.topNav-verticalLinks * { - margin-left: 10px; - font-weight: bold; - font-size: 15px; -} - -@media screen and (max-width: 480px) { - .googleMap { display: none; } -} - -@media screen and (max-width: 380px) { - .commitchangeLogo-text { display: none !important; } - .commitchangeLogo img { width: 32px !important; } -} - diff --git a/app/assets/stylesheets/components/turquoise_header.css.scss b/app/assets/stylesheets/components/turquoise_header.scss similarity index 100% rename from app/assets/stylesheets/components/turquoise_header.css.scss rename to app/assets/stylesheets/components/turquoise_header.scss diff --git a/app/assets/stylesheets/components/type_ahead.css.scss b/app/assets/stylesheets/components/type_ahead.scss similarity index 100% rename from app/assets/stylesheets/components/type_ahead.css.scss rename to app/assets/stylesheets/components/type_ahead.scss diff --git a/app/assets/stylesheets/components/wizard_index.css.scss b/app/assets/stylesheets/components/wizard_index.scss similarity index 100% rename from app/assets/stylesheets/components/wizard_index.css.scss rename to app/assets/stylesheets/components/wizard_index.scss diff --git a/app/assets/stylesheets/coupons/page.css.scss b/app/assets/stylesheets/coupons/page.scss similarity index 100% rename from app/assets/stylesheets/coupons/page.css.scss rename to app/assets/stylesheets/coupons/page.scss diff --git a/app/assets/stylesheets/emails.css b/app/assets/stylesheets/emails.css index c4f2e47eb..8a54e0228 100644 --- a/app/assets/stylesheets/emails.css +++ b/app/assets/stylesheets/emails.css @@ -1,6 +1,5 @@ /* License: LGPL-3.0-or-later */ -body, -.emailWrapper { +body { margin: 0; padding: 0; background: white; @@ -8,10 +7,6 @@ body, line-height: 1.6; color: #30373D; } -.emailWrapper { - height: 100% !important; - width: 100% !important; -} img{ width: auto !important; max-width:100%; diff --git a/app/assets/stylesheets/emails/page.css b/app/assets/stylesheets/emails/page.css new file mode 100644 index 000000000..37894c546 --- /dev/null +++ b/app/assets/stylesheets/emails/page.css @@ -0,0 +1,5 @@ +/* License: LGPL-3.0-or-later */ +/* + *= require ../emails + *= require ../common/vendor/froala_editor + */ \ No newline at end of file diff --git a/app/assets/stylesheets/events/index/page.css.scss b/app/assets/stylesheets/events/index/page.scss similarity index 100% rename from app/assets/stylesheets/events/index/page.css.scss rename to app/assets/stylesheets/events/index/page.scss diff --git a/app/assets/stylesheets/events/listing.css.scss b/app/assets/stylesheets/events/listing.scss similarity index 100% rename from app/assets/stylesheets/events/listing.css.scss rename to app/assets/stylesheets/events/listing.scss diff --git a/app/assets/stylesheets/events/new/index.css.scss b/app/assets/stylesheets/events/new/index.scss similarity index 100% rename from app/assets/stylesheets/events/new/index.css.scss rename to app/assets/stylesheets/events/new/index.scss diff --git a/app/assets/stylesheets/events/show/claim_ticket.css.scss b/app/assets/stylesheets/events/show/claim_ticket.scss similarity index 100% rename from app/assets/stylesheets/events/show/claim_ticket.css.scss rename to app/assets/stylesheets/events/show/claim_ticket.scss diff --git a/app/assets/stylesheets/events/show/page.css.scss b/app/assets/stylesheets/events/show/page.scss similarity index 100% rename from app/assets/stylesheets/events/show/page.css.scss rename to app/assets/stylesheets/events/show/page.scss diff --git a/app/assets/stylesheets/events/show/settings.css.scss b/app/assets/stylesheets/events/show/settings.scss similarity index 100% rename from app/assets/stylesheets/events/show/settings.css.scss rename to app/assets/stylesheets/events/show/settings.scss diff --git a/app/assets/stylesheets/events/stats/page.css.scss b/app/assets/stylesheets/events/stats/page.scss similarity index 100% rename from app/assets/stylesheets/events/stats/page.css.scss rename to app/assets/stylesheets/events/stats/page.scss diff --git a/app/assets/stylesheets/explore/page.css.scss b/app/assets/stylesheets/explore/page.scss similarity index 100% rename from app/assets/stylesheets/explore/page.css.scss rename to app/assets/stylesheets/explore/page.scss diff --git a/app/assets/stylesheets/global.css.scss b/app/assets/stylesheets/global.css.scss deleted file mode 100644 index e89aee507..000000000 --- a/app/assets/stylesheets/global.css.scss +++ /dev/null @@ -1,47 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'resets'; -@import 'common/typography/base'; -@import 'common/typography/special'; - -@import 'components/container'; -@import 'body'; -@import 'components/buttons'; - -@import 'common/images'; - -@import 'components/headers'; -@import 'components/side_nav'; - -@import 'components/footer'; -@import 'common/layouts'; -@import 'common/page'; -@import 'common/utils'; -@import 'common/states'; -@import 'common/icons'; - -@import 'components/announcement_bar'; -@import 'components/notification_alerts'; -@import 'components/forms'; -@import 'components/modals'; -@import 'components/inputs'; -@import 'components/wizard_index'; -@import 'components/decorative'; -@import 'components/progress_bar'; -@import 'components/better_browser'; -@import 'components/parsley'; -@import 'components/tables'; -@import 'components/arrows'; -@import 'components/cards'; -@import 'components/pastel_boxes'; -@import 'components/full_screen_loading'; -@import 'components/loading'; -@import 'components/tooltips'; -@import 'components/top_nav'; - -@import 'common/media_queries'; -@import 'common/z_indices'; -@import 'common/ios_hack'; -@import 'common/minimal'; - -@import 'common/focusable' \ No newline at end of file diff --git a/app/assets/stylesheets/global.scss b/app/assets/stylesheets/global.scss new file mode 100644 index 000000000..7460dffd6 --- /dev/null +++ b/app/assets/stylesheets/global.scss @@ -0,0 +1,53 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'resets'; +@import 'common/typography/base'; +@import 'common/typography/special'; + +@import 'components/container'; +@import 'body'; +@import 'components/buttons'; + +@import 'common/images'; + +@import 'components/headers'; +@import 'components/side_nav'; + +@import 'components/footer'; +@import 'common/layouts'; +@import 'common/page'; +@import 'common/utils'; +@import 'common/states'; +@import 'common/icons'; + +@import 'components/announcement_bar'; +@import 'components/notification_alerts'; +@import 'components/forms'; +@import 'components/modals'; +@import 'components/inputs'; +@import 'components/wizard_index'; +@import 'components/decorative'; +@import 'components/progress_bar'; +@import 'components/better_browser'; +@import 'components/parsley'; +@import 'components/tables'; +@import 'components/arrows'; +@import 'components/cards'; +@import 'components/pastel_boxes'; +@import 'components/full_screen_loading'; +@import 'components/loading'; +@import 'components/tooltips'; + +@import 'common/media_queries'; +@import 'common/z_indices'; +@import 'common/minimal'; + +@import 'common/focusable'; + +.checkbox-feeCoverage-label { + display:flex !important; + align-items: center; + justify-content: center; +} + +.grecaptcha-badge { visibility: hidden; } \ No newline at end of file diff --git a/app/assets/stylesheets/mixins.css.scss b/app/assets/stylesheets/mixins.scss similarity index 100% rename from app/assets/stylesheets/mixins.css.scss rename to app/assets/stylesheets/mixins.scss diff --git a/app/assets/stylesheets/nonprofits/btn/page.css.scss b/app/assets/stylesheets/nonprofits/btn/common.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/btn/page.css.scss rename to app/assets/stylesheets/nonprofits/btn/common.scss diff --git a/app/assets/stylesheets/nonprofits/btn/page.scss.erb b/app/assets/stylesheets/nonprofits/btn/page.scss.erb new file mode 100644 index 000000000..96461dcb7 --- /dev/null +++ b/app/assets/stylesheets/nonprofits/btn/page.scss.erb @@ -0,0 +1,22 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'nonprofits/btn/common'; +@import 'boot/google-webfonts'; + +body { + @include open-sans; + font-size: 16px; + line-height: 1.2; + color: $charcoal; + margin: 0; + height: 100%; +} + + +body.embedded-layout { + background-color: #f8f8f8; + padding: 0; +} + +.centered, .u-centered { + text-align: center !important; +} diff --git a/app/assets/stylesheets/nonprofits/button/page.css.scss b/app/assets/stylesheets/nonprofits/button/page.css.scss deleted file mode 100644 index 841a2d12a..000000000 --- a/app/assets/stylesheets/nonprofits/button/page.css.scss +++ /dev/null @@ -1,144 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'nonprofits/btn/page'; -@import 'components/page_tabs'; -@import 'components/steps_menu'; -@import 'components/help_box'; -@import 'components/animations'; - -.stepsMenu { - margin-top: 15px; - @include columns(3.5); -} -.step { - padding-left: 15px; - @include columns-right(8.5); -} -.step label { - width: 100%; -} -.step-header { - padding: 5px; - text-align: center; -} -.step-header * { - @include open-sans; - font-weight: bold; -} -.step-header-title { - margin: 5px 0; -} -.step-inner { - background: $fog; - border: 1px solid rgba(black, 0.05); - padding: 15px; - overflow: auto; -} -.step-footer { - padding: 15px 15px 0 15px; - text-align: center; -} -.appearance label { - text-align: center; - cursor: pointer; - display: block; - padding: 15px; -} -.appearance img { - max-height: 180px; -} -.appearance table { - width: 100%; -} -.appearance td { - width: 50%; - vertical-align: middle; -} -.appearance td:nth-of-type(1){ - border-right: 1px dashed rgba(black, 0.08); -} -.appearance tr, -.customText-wrapper, -.step.type label, -.step.amounts label, -.step.designations label { - border-bottom: 1px dashed rgba(black, 0.08); -} -.step.designations label, -.step.amounts label { - padding: 10px 0; -} -.customText-wrapper { - margin-bottom: 10px; -} -.appearance input[type='text'] { - margin: 10px 0 0 0; - max-width: 300px; -} -.customText-text { - display: block; - font-size: 20px; - min-height: 25px; -} -.step.type label { - line-height: 30px; - overflow: auto; - cursor: pointer; - position: relative; -} -.step.type img { - vertical-align: middle; - float: right; - width: 25px; - margin: 5px 5px 15px 5px; -} -.step.preview ul { - margin: 10px 0 0 0; -} -.advanced section { - padding: 20px; -} -.advanced section h3 { - text-align: center; - margin: 10px auto; -} -.advanced section h5 { - line-height: 1; - color: rgba($charcoal, 0.9); - margin: 30px 0 10px 0; -} -.advanced textarea { - margin: 0 0 20px 0; -} -.advanced .no-js { - border: 5px $light-pollen solid; - padding: 20px; -} -.advanced .yes-js { - padding: 20px; - margin-top: 40px; - border: 5px $faded-sky solid; -} - -@include keyframes(popUpToFade) { - 8% { @include transform(translateY(3px)); } - 12% { - @include opacity(1); - @include transform(translateY(5px)); - } - 90% { - @include opacity(1); - } - 100% { - visibility: hidden; - @include transform(translateY(5px)); - } -} -.helpBoxWrapper { - @include transform(translateY(200px)); - @include opacity(0); - position: fixed; - bottom: 80px; - right: 60px; - @include animation('popUpToFade 10s ease-out 2.5s forwards'); -} diff --git a/app/assets/stylesheets/nonprofits/button/page.scss b/app/assets/stylesheets/nonprofits/button/page.scss new file mode 100644 index 000000000..eed33cb26 --- /dev/null +++ b/app/assets/stylesheets/nonprofits/button/page.scss @@ -0,0 +1,144 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'nonprofits/btn/common'; +@import 'components/page_tabs'; +@import 'components/steps_menu'; +@import 'components/help_box'; +@import 'components/animations'; + +.stepsMenu { + margin-top: 15px; + @include columns(3.5); +} +.step { + padding-left: 15px; + @include columns-right(8.5); +} +.step label { + width: 100%; +} +.step-header { + padding: 5px; + text-align: center; +} +.step-header * { + @include open-sans; + font-weight: bold; +} +.step-header-title { + margin: 5px 0; +} +.step-inner { + background: $fog; + border: 1px solid rgba(black, 0.05); + padding: 15px; + overflow: auto; +} +.step-footer { + padding: 15px 15px 0 15px; + text-align: center; +} +.appearance label { + text-align: center; + cursor: pointer; + display: block; + padding: 15px; +} +.appearance img { + max-height: 180px; +} +.appearance table { + width: 100%; +} +.appearance td { + width: 50%; + vertical-align: middle; +} +.appearance td:nth-of-type(1){ + border-right: 1px dashed rgba(black, 0.08); +} +.appearance tr, +.customText-wrapper, +.step.type label, +.step.amounts label, +.step.designations label { + border-bottom: 1px dashed rgba(black, 0.08); +} +.step.designations label, +.step.amounts label { + padding: 10px 0; +} +.customText-wrapper { + margin-bottom: 10px; +} +.appearance input[type='text'] { + margin: 10px 0 0 0; + max-width: 300px; +} +.customText-text { + display: block; + font-size: 20px; + min-height: 25px; +} +.step.type label { + line-height: 30px; + overflow: auto; + cursor: pointer; + position: relative; +} +.step.type img { + vertical-align: middle; + float: right; + width: 25px; + margin: 5px 5px 15px 5px; +} +.step.preview ul { + margin: 10px 0 0 0; +} +.advanced section { + padding: 20px; +} +.advanced section h3 { + text-align: center; + margin: 10px auto; +} +.advanced section h5 { + line-height: 1; + color: rgba($charcoal, 0.9); + margin: 30px 0 10px 0; +} +.advanced textarea { + margin: 0 0 20px 0; +} +.advanced .no-js { + border: 5px $light-pollen solid; + padding: 20px; +} +.advanced .yes-js { + padding: 20px; + margin-top: 40px; + border: 5px $faded-sky solid; +} + +@include keyframes(popUpToFade) { + 8% { @include transform(translateY(3px)); } + 12% { + @include opacity(1); + @include transform(translateY(5px)); + } + 90% { + @include opacity(1); + } + 100% { + visibility: hidden; + @include transform(translateY(5px)); + } +} +.helpBoxWrapper { + @include transform(translateY(200px)); + @include opacity(0); + position: fixed; + bottom: 80px; + right: 60px; + @include animation('popUpToFade 10s ease-out 2.5s forwards'); +} diff --git a/app/assets/stylesheets/nonprofits/dashboard/page.css.scss b/app/assets/stylesheets/nonprofits/dashboard/page.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/dashboard/page.css.scss rename to app/assets/stylesheets/nonprofits/dashboard/page.scss diff --git a/app/assets/stylesheets/nonprofits/donate/page.css.scss b/app/assets/stylesheets/nonprofits/donate/page.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/donate/page.css.scss rename to app/assets/stylesheets/nonprofits/donate/page.scss diff --git a/app/assets/stylesheets/nonprofits/donation_form/footer.css.scss b/app/assets/stylesheets/nonprofits/donation_form/footer.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/donation_form/footer.css.scss rename to app/assets/stylesheets/nonprofits/donation_form/footer.scss diff --git a/app/assets/stylesheets/nonprofits/donation_form/form.css.scss b/app/assets/stylesheets/nonprofits/donation_form/form.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/donation_form/form.css.scss rename to app/assets/stylesheets/nonprofits/donation_form/form.scss diff --git a/app/assets/stylesheets/nonprofits/donation_form/show/index.css.scss b/app/assets/stylesheets/nonprofits/donation_form/show/index.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/donation_form/show/index.css.scss rename to app/assets/stylesheets/nonprofits/donation_form/show/index.scss diff --git a/app/assets/stylesheets/nonprofits/donation_form/title_row.css.scss b/app/assets/stylesheets/nonprofits/donation_form/title_row.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/donation_form/title_row.css.scss rename to app/assets/stylesheets/nonprofits/donation_form/title_row.scss diff --git a/app/assets/stylesheets/nonprofits/manage/main_metrics.css.scss b/app/assets/stylesheets/nonprofits/manage/main_metrics.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/manage/main_metrics.css.scss rename to app/assets/stylesheets/nonprofits/manage/main_metrics.scss diff --git a/app/assets/stylesheets/nonprofits/manage/supporter_table.css.scss b/app/assets/stylesheets/nonprofits/manage/supporter_table.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/manage/supporter_table.css.scss rename to app/assets/stylesheets/nonprofits/manage/supporter_table.scss diff --git a/app/assets/stylesheets/nonprofits/payments/index/filter_panel.css.scss b/app/assets/stylesheets/nonprofits/payments/index/filter_panel.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/payments/index/filter_panel.css.scss rename to app/assets/stylesheets/nonprofits/payments/index/filter_panel.scss diff --git a/app/assets/stylesheets/nonprofits/payments/index/main_panel.css.scss b/app/assets/stylesheets/nonprofits/payments/index/main_panel.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/payments/index/main_panel.css.scss rename to app/assets/stylesheets/nonprofits/payments/index/main_panel.scss diff --git a/app/assets/stylesheets/nonprofits/payments/index/page.css.scss b/app/assets/stylesheets/nonprofits/payments/index/page.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/payments/index/page.css.scss rename to app/assets/stylesheets/nonprofits/payments/index/page.scss diff --git a/app/assets/stylesheets/nonprofits/payouts/index/page.css.scss b/app/assets/stylesheets/nonprofits/payouts/index/page.css.scss deleted file mode 100644 index 161b85beb..000000000 --- a/app/assets/stylesheets/nonprofits/payouts/index/page.css.scss +++ /dev/null @@ -1,18 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'pikaday'; -@import 'components/page_tabs'; -@import 'components/nonprofit_bank_accounts'; -@import 'components/todos'; -@import 'components/identity_verification'; - -.payout-history .succeeded { - color: $bluegrass; -} -.payout-history .failed { - color: $red; -} -.payout-history .pending { - color: $oj; -} - diff --git a/app/assets/stylesheets/nonprofits/payouts/index/page.scss b/app/assets/stylesheets/nonprofits/payouts/index/page.scss new file mode 100644 index 000000000..6ea21ef50 --- /dev/null +++ b/app/assets/stylesheets/nonprofits/payouts/index/page.scss @@ -0,0 +1,17 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'pikaday'; +@import 'components/page_tabs'; +@import 'components/nonprofit_bank_accounts'; +@import 'components/todos'; + +.payout-history .succeeded { + color: $bluegrass; +} +.payout-history .failed { + color: $red; +} +.payout-history .pending { + color: $oj; +} + diff --git a/app/assets/stylesheets/nonprofits/recurring_donations/index/page.css.scss b/app/assets/stylesheets/nonprofits/recurring_donations/index/page.css.scss deleted file mode 100644 index 89e85936c..000000000 --- a/app/assets/stylesheets/nonprofits/recurring_donations/index/page.css.scss +++ /dev/null @@ -1,24 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'recurring_donations/modal'; -@import 'recurring_donations/edit'; -@import 'pikaday'; -@import 'supporters/form'; -@import 'bootstrap-tour'; -@import 'components/page_tabs'; -@import 'components/pagination'; -@import 'components/panels_layout'; - -.sidePanel, -.panelsLayout.is-showingSidePanel .mainPanel { - width: 50%; -} -.panelsLayout.is-showingSidePanel .button--closeSidePanel { - right: 50%; -} -.panelsLayout.is-showingSidePanel .hiddenWhenExpanded { - display: none; -} -.sidePanel table td:first-of-type { - font-weight: bold; -} diff --git a/app/assets/stylesheets/nonprofits/recurring_donations/index/page.scss b/app/assets/stylesheets/nonprofits/recurring_donations/index/page.scss new file mode 100644 index 000000000..f4ff10eaa --- /dev/null +++ b/app/assets/stylesheets/nonprofits/recurring_donations/index/page.scss @@ -0,0 +1,23 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'recurring_donations/modal'; +@import 'recurring_donations/edit'; +@import 'pikaday'; +@import 'bootstrap-tour'; +@import 'components/page_tabs'; +@import 'components/pagination'; +@import 'components/panels_layout'; + +.sidePanel, +.panelsLayout.is-showingSidePanel .mainPanel { + width: 50%; +} +.panelsLayout.is-showingSidePanel .button--closeSidePanel { + right: 50%; +} +.panelsLayout.is-showingSidePanel .hiddenWhenExpanded { + display: none; +} +.sidePanel table td:first-of-type { + font-weight: bold; +} diff --git a/app/assets/stylesheets/nonprofits/show/page.css.scss b/app/assets/stylesheets/nonprofits/show/page.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/show/page.css.scss rename to app/assets/stylesheets/nonprofits/show/page.scss diff --git a/app/assets/stylesheets/nonprofits/show/settings_modal.css.scss b/app/assets/stylesheets/nonprofits/show/settings_modal.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/show/settings_modal.css.scss rename to app/assets/stylesheets/nonprofits/show/settings_modal.scss diff --git a/app/assets/stylesheets/nonprofits/supporter_form/page.css.scss b/app/assets/stylesheets/nonprofits/supporter_form/page.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/supporter_form/page.css.scss rename to app/assets/stylesheets/nonprofits/supporter_form/page.scss diff --git a/app/assets/stylesheets/nonprofits/supporters/custom_fields.css.scss b/app/assets/stylesheets/nonprofits/supporters/custom_fields.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/supporters/custom_fields.css.scss rename to app/assets/stylesheets/nonprofits/supporters/custom_fields.scss diff --git a/app/assets/stylesheets/nonprofits/supporters/index/email_modal.css.scss b/app/assets/stylesheets/nonprofits/supporters/index/email_modal.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/supporters/index/email_modal.css.scss rename to app/assets/stylesheets/nonprofits/supporters/index/email_modal.scss diff --git a/app/assets/stylesheets/nonprofits/supporters/index/page.css.scss b/app/assets/stylesheets/nonprofits/supporters/index/page.css.scss deleted file mode 100644 index 2cc0a0ec8..000000000 --- a/app/assets/stylesheets/nonprofits/supporters/index/page.css.scss +++ /dev/null @@ -1,83 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'components/pagination'; -@import 'supporters/form'; -@import 'pikaday'; -@import 'components/panels_layout'; -@import 'side_panel'; -@import 'components/tags'; -@import 'components/bulk_actions'; -@import 'components/page_tabs'; -@import 'components/toggle_buttons'; -@import 'components/drop_down'; -@import 'components/google_maps'; -@import 'common/editable'; -@import 'nonprofits/supporters/custom_fields'; -@import 'components/tables/filtering/meta_status'; -@import 'bootstrap-tour'; - -.supportersMap { - border: 2px solid rgba(black, 0.2); - position: relative; -} -.supportersMap .legend { - @include box-shadow(-2px 2px 4px 0 rgba(black, 0.07)); - border-bottom: 2px solid rgba(black, 0.2); - border-left: 2px solid rgba(black, 0.2); - position: absolute; - right: 0; - top: 0; - background: $fog; - width: 30%; - padding: 7px; - z-index: 1; -} -.supportersMap .legend .detail { - margin-bottom: 5px; - font-size: 13px; - word-wrap: break-word; -} -.supportersMap .legend .detail i { - margin-right: 5px; - color: grey; -} - -tr:hover .td--name > span { - text-decoration: underline; - -moz-text-decoration-color: rgba(black, 0.2); - text-decoration-color: rgba(black, 0.2); -} - -.headerWithProfile { - border-bottom: none; -} - -.mainPanel .td--action { - width: 30px; - padding: 0 15px; -} - -.mainPanel .td--action-email { - background: white; - border: 1px solid rgba(black, 0.1); - font-size: 14px; - color: $logo-blue; - text-align: center; - line-height: 25px; - width: 26px; - height: 26px; - @include border-radius(50%); -} - -.mainPanel input[type='checkbox'] + label:before { - margin: 0; -} - -.panelsLayout.is-showingSidePanel { - .mainPanel .th--name, - .mainPanel .td--name { - font-size: 16px; - } -} - - diff --git a/app/assets/stylesheets/nonprofits/supporters/index/page.scss b/app/assets/stylesheets/nonprofits/supporters/index/page.scss new file mode 100644 index 000000000..52b50530b --- /dev/null +++ b/app/assets/stylesheets/nonprofits/supporters/index/page.scss @@ -0,0 +1,82 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'components/pagination'; +@import 'pikaday'; +@import 'components/panels_layout'; +@import 'side_panel'; +@import 'components/tags'; +@import 'components/bulk_actions'; +@import 'components/page_tabs'; +@import 'components/toggle_buttons'; +@import 'components/drop_down'; +@import 'components/google_maps'; +@import 'common/editable'; +@import 'nonprofits/supporters/custom_fields'; +@import 'components/tables/filtering/meta_status'; +@import 'bootstrap-tour'; + +.supportersMap { + border: 2px solid rgba(black, 0.2); + position: relative; +} +.supportersMap .legend { + @include box-shadow(-2px 2px 4px 0 rgba(black, 0.07)); + border-bottom: 2px solid rgba(black, 0.2); + border-left: 2px solid rgba(black, 0.2); + position: absolute; + right: 0; + top: 0; + background: $fog; + width: 30%; + padding: 7px; + z-index: 1; +} +.supportersMap .legend .detail { + margin-bottom: 5px; + font-size: 13px; + word-wrap: break-word; +} +.supportersMap .legend .detail i { + margin-right: 5px; + color: grey; +} + +tr:hover .td--name > span { + text-decoration: underline; + -moz-text-decoration-color: rgba(black, 0.2); + text-decoration-color: rgba(black, 0.2); +} + +.headerWithProfile { + border-bottom: none; +} + +.mainPanel .td--action { + width: 30px; + padding: 0 15px; +} + +.mainPanel .td--action-email { + background: white; + border: 1px solid rgba(black, 0.1); + font-size: 14px; + color: $logo-blue; + text-align: center; + line-height: 25px; + width: 26px; + height: 26px; + @include border-radius(50%); +} + +.mainPanel input[type='checkbox'] + label:before { + margin: 0; +} + +.panelsLayout.is-showingSidePanel { + .mainPanel .th--name, + .mainPanel .td--name { + font-size: 16px; + } +} + + diff --git a/app/assets/stylesheets/nonprofits/supporters/index/side_panel.css.scss b/app/assets/stylesheets/nonprofits/supporters/index/side_panel.scss similarity index 100% rename from app/assets/stylesheets/nonprofits/supporters/index/side_panel.css.scss rename to app/assets/stylesheets/nonprofits/supporters/index/side_panel.scss diff --git a/app/assets/stylesheets/nonprofits/supporters/index/timeline.css.scss b/app/assets/stylesheets/nonprofits/supporters/index/timeline.css.scss deleted file mode 100644 index f26ab34e4..000000000 --- a/app/assets/stylesheets/nonprofits/supporters/index/timeline.css.scss +++ /dev/null @@ -1,82 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; - -ul.timeline-activities { - margin: 0; - position: relative; -} - -.timeline-activities:before { - left: 12px; - top: -20px; - height: 20px; - width: 1px; -} - -.timeline-actions { - margin-bottom: 20px; - border: 1px solid rgba(black, 0.1); - background: rgba(black, 0.03); -} -.timeline-activity { - padding: 0 0 16px 32px; - position: relative; -} -.timeline-activities:before, -.timeline-activity:after, -.timeline-activity:before { - position: absolute; - content: ''; - background: rgb(230, 230, 230); -} -.timeline-activity:after { - left: 25px; - top: 12px; - height: 1px; - width: 7px; -} -.timeline-activity:before { - left: 12px; - top: 0; - height: 100%; - width: 1px; -} - -// line connecting the icons -.timeline-activity:last-of-type:before { - display: none; -} - -.timeline-activity-icon { - background: #F8F7F2; - border: 1px solid rgb(230, 230, 230); - width: 25px; height: 25px; - @include border-radius(50%); - position: absolute; - text-align: center; - top: 0; - left: 0; -} - -.timeline-activity-icon i { - font-size: 12px; - color: rgba(black, 0.4); - @include transform(translateY(-1px)); -} -.timeline-activity-icon i.fa-ticket, -.timeline-activity-icon i.fa-pencil { - font-size: 14px; - @include transform(translateY(-2px)); -} - -.timeline .timeline-activity-card p { - margin-bottom: 0; -} - -.timeline-activity-card { - background: white; - padding: 8px 10px; - line-height: 20px; - border: 1px solid rgba(black, 0.1); -} - diff --git a/app/assets/stylesheets/nonprofits/supporters/index/timeline.scss b/app/assets/stylesheets/nonprofits/supporters/index/timeline.scss new file mode 100644 index 000000000..d136e736e --- /dev/null +++ b/app/assets/stylesheets/nonprofits/supporters/index/timeline.scss @@ -0,0 +1,87 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; + +ul.timeline-activities { + margin: 0; + position: relative; +} + +.timeline-activities:before { + left: 12px; + top: -20px; + height: 20px; + width: 1px; +} + +.timeline-actions { + margin-bottom: 20px; + border: 1px solid rgba(black, 0.1); + background: rgba(black, 0.03); +} +.timeline-activity { + padding: 0 0 16px 32px; + position: relative; +} +.timeline-activities:before, +.timeline-activity:after, +.timeline-activity:before { + position: absolute; + content: ''; + background: rgb(230, 230, 230); +} +.timeline-activity:after { + left: 25px; + top: 12px; + height: 1px; + width: 7px; +} +.timeline-activity:before { + left: 12px; + top: 0; + height: 100%; + width: 1px; +} + +// line connecting the icons +.timeline-activity:last-of-type:before { + display: none; +} + +.timeline-activity-icon { + background: #F8F7F2; + border: 1px solid rgb(230, 230, 230); + width: 25px; height: 25px; + @include border-radius(50%); + position: absolute; + text-align: center; + top: 0; + left: 0; +} + +.timeline-activity-icon i { + font-size: 12px; + color: rgba(black, 0.4); + @include transform(translateY(-1px)); +} +.timeline-activity-icon i.fa-ticket, +.timeline-activity-icon i.fa-pencil { + font-size: 14px; + @include transform(translateY(-2px)); +} + +.timeline .timeline-activity-card p { + margin-bottom: 0; +} + +.timeline-activity-card { + background: white; + padding: 8px 10px; + line-height: 20px; + border: 1px solid rgba(black, 0.1); +} + +.timeline-activity-card div.activity-section { + line-height: 1.3em; + margin-bottom: 5px; +} + diff --git a/app/assets/stylesheets/profiles/show/page.css.scss b/app/assets/stylesheets/profiles/show/page.scss similarity index 100% rename from app/assets/stylesheets/profiles/show/page.css.scss rename to app/assets/stylesheets/profiles/show/page.scss diff --git a/app/assets/stylesheets/recurring_donations/edit.css.scss b/app/assets/stylesheets/recurring_donations/edit.scss similarity index 100% rename from app/assets/stylesheets/recurring_donations/edit.css.scss rename to app/assets/stylesheets/recurring_donations/edit.scss diff --git a/app/assets/stylesheets/recurring_donations/edit/page.css.scss b/app/assets/stylesheets/recurring_donations/edit/page.scss similarity index 100% rename from app/assets/stylesheets/recurring_donations/edit/page.css.scss rename to app/assets/stylesheets/recurring_donations/edit/page.scss diff --git a/app/assets/stylesheets/recurring_donations/modal.css.scss b/app/assets/stylesheets/recurring_donations/modal.css.scss deleted file mode 100644 index 76b6ce573..000000000 --- a/app/assets/stylesheets/recurring_donations/modal.css.scss +++ /dev/null @@ -1,2 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'supporters/form'; diff --git a/app/assets/stylesheets/recurring_donations/modal.scss b/app/assets/stylesheets/recurring_donations/modal.scss new file mode 100644 index 000000000..632a7e1ba --- /dev/null +++ b/app/assets/stylesheets/recurring_donations/modal.scss @@ -0,0 +1,2 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ + diff --git a/app/assets/stylesheets/resets.css.scss b/app/assets/stylesheets/resets.scss similarity index 100% rename from app/assets/stylesheets/resets.css.scss rename to app/assets/stylesheets/resets.scss diff --git a/app/assets/stylesheets/settings/index/branding.css.scss b/app/assets/stylesheets/settings/index/branding.css.scss deleted file mode 100644 index 4bf6c2ffb..000000000 --- a/app/assets/stylesheets/settings/index/branding.css.scss +++ /dev/null @@ -1,80 +0,0 @@ -/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ -@import 'mixins'; -@import 'common/vendor/colpick'; -@import 'nonprofits/btn/page'; -@import 'common/branded_campaign_button'; - - -.branding-settings-wrapper { - float: left; - width: 244px; -} - -.branding-settings-wrapper .title, -.preview-wrapper .title { - padding: 8px; - font-weight: 600; - margin: 0; -} -.preview-wrapper .title { - text-align: center; -} - -.font-wrapper ul { margin: 0; } - -.font-wrapper li { - padding: 5px 10px; - color: rgba(black, 0.6); - position: relative; - cursor: pointer; - &.open {font-family: OpenSansCondensed, 'Helvetica Neue', Arial, sans-serif;} - &.helvetica {font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;} - &.georgia {font-family: Georgia, Serif;} - &.futura {font-family: ‘Futura’, Arial, sans-serif;} - &.bitter {@include bitter;} - &:after{ - content: ''; - position: absolute; - bottom: 0; - left: 0; - width: 100%; - height: 1px; - background: rgba($grey, 0.1); - } - &:hover { - font-weight: bold; - } -} - -.color-wrapper { - overflow: auto; -} - -// color picker -.colPick-wrapper { - height: 240px; - padding: 20px; -} -.colPick-wrapper input[type="text"]:focus { - border: none; -} - -.preview-wrapper { - overflow: auto; - @include noselect; -} - -.branded-donate-button-wrapper { - text-align: center; - padding: 20px; -} - -.pane-inner .branded-donate-button { - @include transition(none); -} - -.branding-form { - margin: 0; - text-align: center; - padding: 15px 10px 10px; -} diff --git a/app/assets/stylesheets/settings/index/branding.scss b/app/assets/stylesheets/settings/index/branding.scss new file mode 100644 index 000000000..0e1db6cf6 --- /dev/null +++ b/app/assets/stylesheets/settings/index/branding.scss @@ -0,0 +1,80 @@ +/* License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later */ +@import 'mixins'; +@import 'common/vendor/colpick'; +@import 'nonprofits/btn/common'; +@import 'common/branded_campaign_button'; + + +.branding-settings-wrapper { + float: left; + width: 244px; +} + +.branding-settings-wrapper .title, +.preview-wrapper .title { + padding: 8px; + font-weight: 600; + margin: 0; +} +.preview-wrapper .title { + text-align: center; +} + +.font-wrapper ul { margin: 0; } + +.font-wrapper li { + padding: 5px 10px; + color: rgba(black, 0.6); + position: relative; + cursor: pointer; + &.open {font-family: OpenSansCondensed, 'Helvetica Neue', Arial, sans-serif;} + &.helvetica {font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;} + &.georgia {font-family: Georgia, Serif;} + &.futura {font-family: ‘Futura’, Arial, sans-serif;} + &.bitter {@include bitter;} + &:after{ + content: ''; + position: absolute; + bottom: 0; + left: 0; + width: 100%; + height: 1px; + background: rgba($grey, 0.1); + } + &:hover { + font-weight: bold; + } +} + +.color-wrapper { + overflow: auto; +} + +// color picker +.colPick-wrapper { + height: 240px; + padding: 20px; +} +.colPick-wrapper input[type="text"]:focus { + border: none; +} + +.preview-wrapper { + overflow: auto; + @include noselect; +} + +.branded-donate-button-wrapper { + text-align: center; + padding: 20px; +} + +.pane-inner .branded-donate-button { + @include transition(none); +} + +.branding-form { + margin: 0; + text-align: center; + padding: 15px 10px 10px; +} diff --git a/app/assets/stylesheets/settings/index/page.css.scss b/app/assets/stylesheets/settings/index/page.scss similarity index 100% rename from app/assets/stylesheets/settings/index/page.css.scss rename to app/assets/stylesheets/settings/index/page.scss diff --git a/app/assets/stylesheets/tickets/form.css.scss b/app/assets/stylesheets/tickets/form.css.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/app/assets/stylesheets/supporters/form.css.scss b/app/assets/stylesheets/tickets/form.scss similarity index 100% rename from app/assets/stylesheets/supporters/form.css.scss rename to app/assets/stylesheets/tickets/form.scss diff --git a/app/assets/stylesheets/tickets/index/page.css.scss b/app/assets/stylesheets/tickets/index/page.scss similarity index 100% rename from app/assets/stylesheets/tickets/index/page.css.scss rename to app/assets/stylesheets/tickets/index/page.scss diff --git a/app/assets/svgs/donate-button.svg b/app/assets/svgs/donate-button.svg index 4459d645a..d2dd3abc1 100644 --- a/app/assets/svgs/donate-button.svg +++ b/app/assets/svgs/donate-button.svg @@ -1,106 +1 @@ - - - - - - - - image/svg+xml - - - - - - - - - Donate - - - - - +Donate \ No newline at end of file diff --git a/app/controllers/activities_controller.rb b/app/controllers/activities_controller.rb deleted file mode 100644 index 07dd5f2c4..000000000 --- a/app/controllers/activities_controller.rb +++ /dev/null @@ -1,10 +0,0 @@ -# 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] - - def create - json_saved Activity.create(params[:activity]) - end - -end diff --git a/app/controllers/api_new/api_controller.rb b/app/controllers/api_new/api_controller.rb new file mode 100644 index 000000000..827f73a0c --- /dev/null +++ b/app/controllers/api_new/api_controller.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module ApiNew + class ApiController < ActionController::Base # rubocop:disable Rails/ApplicationController + # We disable Rails/ApplicationController because we don't want all the stuff in ApplicationController included since + # the Api is simpler + include Controllers::Locale + include Controllers::Nonprofit::Authorization + include Controllers::ApiNew::JbuilderExpansions + rescue_from ActiveRecord::RecordInvalid, with: :record_invalid_rescue + rescue_from AuthenticationError, with: :unauthorized_rescue + + protected + + def record_invalid_rescue(error) + render json: { errors: error.record.errors.messages }, status: :unprocessable_entity + end + + def unauthorized_rescue(error) + @error = error + render 'api_new/errors/unauthorized', status: :unauthorized + end + end +end \ No newline at end of file diff --git a/app/controllers/api_new/object_events_controller.rb b/app/controllers/api_new/object_events_controller.rb new file mode 100644 index 000000000..7677f99d7 --- /dev/null +++ b/app/controllers/api_new/object_events_controller.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +module ApiNew + class ObjectEventsController < ApiNew::ApiController + include Controllers::ApiNew::Nonprofit::Current + include Controllers::Nonprofit::Authorization + before_action :authenticate_nonprofit_user! + + has_scope :event_entity + has_scope :event_types, type: :array + + # Gets the nonprofits object events + # If not logged in, causes a 401 error + def index + @object_events = apply_scopes(current_nonprofit + .associated_object_events) + .order('created_at DESC').page(params[:page]).per(params[:per]) + end + end +end diff --git a/app/controllers/api_new/supporters_controller.rb b/app/controllers/api_new/supporters_controller.rb new file mode 100644 index 000000000..d41e42c4a --- /dev/null +++ b/app/controllers/api_new/supporters_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +module ApiNew + # A controller for interacting with a nonprofit's supporters + class SupportersController < ApiNew::ApiController + include Controllers::ApiNew::Nonprofit::Current + include Controllers::Nonprofit::Authorization + before_action :authenticate_nonprofit_user! + + # Gets the nonprofits supporters + # If not logged in, causes a 401 error + def index + @supporters = current_nonprofit.supporters.order('id DESC').page(params[:page]).per(params[:per]) + end + + # Gets the a single nonprofit supporter + # If not logged in, causes a 401 error + def show + @supporter = current_nonprofit.supporters.find_by(houid:params[:id]) + end + end +end diff --git a/app/controllers/api_new/transactions_controller.rb b/app/controllers/api_new/transactions_controller.rb new file mode 100644 index 000000000..4579cfa5f --- /dev/null +++ b/app/controllers/api_new/transactions_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# A controller for interacting with a nonprofit's supporters +class ApiNew::TransactionsController < ApiNew::ApiController + include Controllers::ApiNew::Transaction::Current + include Controllers::Nonprofit::Authorization + before_action :authenticate_nonprofit_user! + + # Gets the nonprofits supporters + # If not logged in, causes a 401 error + def index + set_json_expansion_paths('supporter', 'subtransaction.payments', 'transaction_assignments', 'payments') + @transactions = current_nonprofit.transactions.order('updated_at DESC').page(params[:page]).per(params[:per]) + end + + # Gets the a single nonprofit supporter + # If not logged in, causes a 401 error + def show + set_json_expansion_paths('supporter', 'subtransaction.payments', 'transaction_assignments', 'payments') + @transaction = current_transaction + end +end \ No newline at end of file diff --git a/app/controllers/api_new/users_controller.rb b/app/controllers/api_new/users_controller.rb new file mode 100644 index 000000000..8178a5a7f --- /dev/null +++ b/app/controllers/api_new/users_controller.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class ApiNew::UsersController < ApiNew::ApiController + include Controllers::User::Authorization + + before_action :authenticate_user! + + # Returns the current user as JSON + # If not logged in, causes a 401 error + def current + @user = current_user + end + + # get /api_new/users/current_nonprofit_object_events + def current_nonprofit_object_events + np_houid = current_user.roles.where(host_type: 'Nonprofit').first&.host&.houid + if np_houid + redirect_to api_new_nonprofit_object_events_path(np_houid, request.query_parameters) + else + render :text => 'Not Found', :status => :not_found + end + end +end diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 76f75165a..352045d89 100755 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -1,15 +1,13 @@ # 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 + protect_from_forgery with: :exception helper_method \ :current_role?, :current_nonprofit_user?, - :administered_nonprofit, - :nonprofit_in_trial?, - :current_plan_tier #int + :administered_nonprofit def set_locale if params[:locale] && Settings.available_locales.include?(params[:locale]) @@ -124,21 +122,6 @@ def current_role?(role_names, host_id = nil) 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) - 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 key = "administered_nonprofit_user_#{current_user_id}_nonprofit" @@ -171,4 +154,11 @@ def current_user_id current_user && current_user.id end + # Overload handle_unverified_request to ensure that + # exception is raised each time a request does not + # pass validation. + def handle_unverified_request + Airbrake.notify(ActionController::InvalidAuthenticityToken, params: params) + + end end diff --git a/app/controllers/aws_presigned_posts_controller.rb b/app/controllers/aws_presigned_posts_controller.rb index bdea08399..c35ae2d23 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 @@ -9,9 +9,9 @@ def create uuid = SecureRandom.uuid p = S3Bucket.presigned_post({ key: "tmp/#{uuid}/${filename}", - success_action_status: 201, + success_action_status: "201", acl: 'public-read', - expiration: 30.days.from_now + expires: 30.days.from_now }) render json: { diff --git a/app/controllers/billing_subscriptions_controller.rb b/app/controllers/billing_subscriptions_controller.rb index 1f63fb4b1..dc9e1af0f 100644 --- a/app/controllers/billing_subscriptions_controller.rb +++ b/app/controllers/billing_subscriptions_controller.rb @@ -2,22 +2,7 @@ class BillingSubscriptionsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_admin! - - def create_trial - render JsonResp.new(params){|params| - requires(:nonprofit_id).as_int - requires(:stripe_plan_id).as_string - }.when_valid{|params| - InsertBillingSubscriptions.trial(params[:nonprofit_id], params[:stripe_plan_id]) - } - end - - 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 + before_action :authenticate_nonprofit_admin! # post /nonprofits/:nonprofit_id/billing_subscription/cancel def cancel diff --git a/app/controllers/campaign_gift_options_controller.rb b/app/controllers/campaign_gift_options_controller.rb index 47b213676..044aab0b2 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 e413855fe..037adcded 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: [:index] + before_action :authenticate_campaign_editor!, only: [:index] def index respond_to do |format| diff --git a/app/controllers/campaigns/donations_controller.rb b/app/controllers/campaigns/donations_controller.rb index 44cb27a10..a53c0f3db 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 9796e687a..5f77ffd06 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 861370a79..6edd58306 100644 --- a/app/controllers/campaigns_controller.rb +++ b/app/controllers/campaigns_controller.rb @@ -3,9 +3,10 @@ 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] + after_action :set_access_control_headers, only: [:metrics] def index @nonprofit = current_nonprofit @@ -76,6 +77,7 @@ def update params[:campaign][:end_datetime] = Chronic.parse(params[:campaign][:end_datetime]) if params[:campaign][:end_datetime].present? end current_campaign.update_attributes params[:campaign] + json_saved current_campaign, 'Successfully updated!' end @@ -138,4 +140,9 @@ def check_nonprofit_status raise ActionController::RoutingError.new('Not Found') end end + + def set_access_control_headers + headers['Access-Control-Allow-Origin'] = "*" + headers['Access-Control-Request-Method'] = %w{GET}.join(",") + end end diff --git a/app/controllers/cards_controller.rb b/app/controllers/cards_controller.rb index 4e84fd1e5..fa75ca58f 100755 --- a/app/controllers/cards_controller.rb +++ b/app/controllers/cards_controller.rb @@ -1,11 +1,16 @@ # 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] + before_action :verify_via_recaptcha!, only: [:create] + before_action :validate_allowed!, only: [:create] + + rescue_from ::Recaptcha::RecaptchaError, with: :handle_recaptcha_failure + + rescue_from ::TempBlockError, with: :handle_temp_block_error # post /cards - def create - acct = Supporter.find(params[:card][:holder_id]).nonprofit.stripe_account_id + def create render( JsonResp.new(params) do |d| requires(:card).nested do @@ -14,9 +19,48 @@ def create requires(:holder_type).one_of('Supporter') end end.when_valid do |d| + supporter = Supporter.find(d[:card][:holder_id]) + acct = supporter.nonprofit.stripe_account_id InsertCard.with_stripe(d[:card], acct, params[:event_id], current_user) end ) end + private + def verify_via_recaptcha! + begin + verify_recaptcha!(action: 'create_card', minimum_score: ENV['MINIMUM_RECAPTCHA_SCORE'].to_f) + rescue ::Recaptcha::RecaptchaError => e + supporter_id = params.try(:card).try(:holder_id) + failure_details = { + supporter: supporter_id, + params: params, + action: 'create_card', + minimum_score_required: ENV['MINIMUM_RECAPTCHA_SCORE'], + recaptcha_result: recaptcha_reply, + recaptcha_value: params['g-recaptcha-response'] + } + failure = RecaptchaRejection.new + failure.details = failure_details + failure.save! + raise e + end + end + + def handle_recaptcha_failure + render json: {error: "There was an temporary error preventing your payment. Please try again. If it persists, please contact support@commitchange.com with error code: 5X4J "}, status: :unprocessable_entity + end + + def handle_temp_block_error + render json: {error: "no"}, status: :unprocessable_entity + end + + def validate_allowed! + if params.dig(:card, :holder_id) + s = Supporter.find(params[:card][:holder_id]) + if s.nonprofit&.miscellaneous_np_info&.temp_block + raise TempBlockError + end + end + end end diff --git a/app/controllers/concerns/controllers/api_new/jbuilder_expansions.rb b/app/controllers/concerns/controllers/api_new/jbuilder_expansions.rb new file mode 100644 index 000000000..cafd4dd93 --- /dev/null +++ b/app/controllers/concerns/controllers/api_new/jbuilder_expansions.rb @@ -0,0 +1,435 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +=begin + +This concern simplifies creating JBuilder objects. + +## The format for child objects in Houdini API + +Child objects for attributes for the Houdini API follow a format based upon the Stripe API +When a child is not expanded it will consist only of the Houid +For example consider in the following object, child is not expanded: +```json +{ + child: "chd_21n45ho...", + id: "familyobj_4532154ni" +} +``` + +However when a child is expanded it will look like this: + +```json +{ + child: { + id: "chd_21n45ho...", + additional_attribute: 1, + parent: "prt_352159", + subchildren: ["subchd_235n158...", "subchd_203213598..."] + }, + id: "familyobj_4532154ni" +} +``` + +### JSON Simplified Path (SPath) +a JSON Simplified Path (SPath) is a bit like dot object notation for JSON objects. For example +If you wanted to reference the child attribute above, you would use the very simple spath of +`child` + +If you wanted to reference the child's id attribute, you would use `child.id` + +One characteristic of SPaths is how they handle arrays. If an spath references an array, it's +actually referencing every item in the array. This will matter later in this code. + +###Combining SPaths and expansion +Now that have SPaths you may be wondering how this relates to expandable attributes. Based upon some input, +we may want decided at runtime whether expand an attribute when rendering Jbuilder templates SPaths allow us to define which items are expanded. To do so, +we go to our Controller action and provide the SPaths of which attributes we want expanded in the outputted object. We do this by passing the +the SPaths to the `set_json_expansion_paths` method. So, if we want to expand the `child` from above, we would call: +`set_json_expansion_paths('child')` + ```json +{ + child: { + id: "chd_21n45ho...", + additional_attribute: 1, + parent: { + id: "prt_352159" + some_other_attribute: "attrib to remember" + }, + subchildren: ["subchd_235n158...", "subchd_203213598..."] + }, + id: "familyobj_4532154ni" +} +``` + +What if we want to expand the parent attribute inside child? We can pass the SPath as follows:`set_json_expansion_paths('child.parent')`. And we get the following: + +Notice that we +don't have to pass child separately. Our expansions know to expand everything back to the root of our Jbuilder value. Your could pass `set_json_expansion_paths('child', 'child.parent')` + +What happens for arrays? We can pass the SPath for subchildren using the same notation `set_json_expansion_paths('child.subchildren')`: + +```json +{ + child: { + id: "chd_21n45ho...", + additional_attribute: 1, + parent: "prt_352159", + subchildren: [ + { + id: "subchd_235n158...", + value: 22942190 + }, + { + id: "subchd_203213598...", + value: 312539 + } + ] + }, + id: "familyobj_4532154ni" +} +``` + +###Expansion and partials +You may be wondering how we know what to generate when we ask for an object to be expanded. We use a feature of JBuidler +where, which selects the partial based upon the class of the object passed to `partial!`. For example, +if you call `json.partial! object` and object is of type Parent, Jbuilder knows that you want to use the partial at `app/views/parents/_parent.json.jbuilder`. + +### How to use this all in practice on a controller action +Using this in practice requires a few steps but is honestly pretty straightforward. + +#### 1. Make sure there is a #to_houid method on the object +Remember, for an object to be shrunk, we need to be able to output the value #to_houid. So we need that for any object which could be expanded. + +#### 2. Make sure to call `#set_json_expansion_paths` in your action with the SPaths you want to expand. + +In your controller action, you need to set which SPaths you want to expand. You can set these based upon parameters passed in (you may want to limit the depth however) +or simply have a default value. + +```ruby +# in `app/controllers/family_objects_controller.rb` +class FamilyObjectsController < ApplicationController + include Controllers::ApiNew::JbuilderExpansions + + def show + set_json_expansion_paths('child') # we just want to expand the child attribute + @family_object = FamilyObject.find(.........) # assume you get the family object you want here + end +end +```` + +#### 3. In your JBuilder view template for the show action make sure to add `__expand: @expand` to the end of your render partial call + +```ruby +# in `app/views/family_objects/show.json.jbuilder` +json.partial! @family_object, as: :family_object, __expand: @__expand +``` + +#### 4. Starting at the partial called in your show template, add `#handle_expansion` and `#handle_array_expansion` calls + +```ruby +# in `app/views/family_objects/_family_object.json.jbuilder` + +json.id family_object.to_houid + +handle_expansion(:child, family_object, {json: json, __expand: __expand}) # make sure json and __expand are there! +``` + +#### 5. Continue adding expansion calls in any partials referred to by the initial partial or any partials it references recursively + +```ruby +# in `app/views/children/_child.json.jbuilder` +json.id child.to_houid + +json.(child, :additional_attribute) + +handle_expansion(:parent, child, {json: json, __expand: __expand}) # make sure json and __expand are there! + +handle_array_expansion(:subchildren, child, {as: :child, json: json, __expand: __expand}) # We're assuming each of subchildren is actually a Child object + # make sure json and __expand are there! + +``` + +```ruby +# in `app/views/parents/_parent.json.jbuilder` +json.id parent.to_houid + +json.some_other_attribute parent.some_other_attribute +``` +=end + + +module Controllers::ApiNew::JbuilderExpansions + extend ActiveSupport::Concern + included do + + @__expand = Controllers::ApiNew::JbuilderExpansions::ExpansionTree.new + + # The SPaths for the expandable attributes you would like to expand. By default, no attributes are expanded. + # You can call this multiple times and new calls will override any previous calls + def set_json_expansion_paths(*expansions) + @__expand = Controllers::ApiNew::JbuilderExpansions.build_json_expansion_path_tree(*expansions) + end + + # Builds the {ExpansionTree}. This is rarely needed but could be helpful in certain specific cases + # @param [Array,Array] paths the paths to expand. If the first item is a {String} then this is an array of SPaths. If the first item is + # an {ExpansionTree}, this object returns that item + # @return [ExpansionTree] + def build_json_expansion_path_tree(*paths) + Controllers::ApiNew::JbuilderExpansions.build_json_expansion_path_tree(*paths) + end + + # Configures an attribute which can be expanded in an jbuilder template + # When shrunk, if you pass this is the equivalent of writing: + # `json.set! attribute, source&.to_houid` # This assumes you passed the :attribute as the attribute + # When expanded, this is the equivalent of: + # `json.set! attribute do + # json.partial! opts[:object] + # end` + # + # @param [Symbol] attribute the name of the attribute that you would like output. + # @param [any] source an object which with an attribute which can be expanded + # @option opts [JbuilderTemplate] :json the JBuilder object being used to generate the outputted JSON This is required. + # @option opts [ExpansionTree] :__expand the ExpansionTree for this current template. This is required. + # @option opts [Symbol] :as the method we call on 'object' when expanding the node. + def handle_expansion(attribute, source, opts={}) + opts = opts.deep_symbolize_keys + opts[:__expand] ||= __expand + ExpansionTreeVisitor.handle_expansion(attribute, source, opts) + end + + # configures an attribute for an array which can be expanded in a jbuilder template + # + # When shrunk, this is the equivalent of writing: + # ```ruby + # json.set! attribute, source.map{|i| i&.to_houid} + # ```` + # When expanded, with the helper of this is the equivalent of writing: + # ```ruby + # json.(attribute) source do |item| # assumes opts[:as] is the same as "attribute" + # json.partial! item + # end + # @param [Symbol] attribute the name of the attribute that you would like output. + # @param [Enumerable] source an enumerable of objects, each of which has an attribute which can be expanded + # @option opts [JbuilderTemplate] :json the JBuilder object being used to generate the outputted JSON This is required. + # @option opts [ExpansionTree] :__expand the ExpansionTree for this current template. This is required. + # @option opts [Symbol] :as_item the method we call on each of the array items as part of a #handle_array_expansion call. + # @yieldparam [ItemExpansion] item if block is passed, an ItemExpansion will be yielded for each item for in the partial + def handle_array_expansion(attribute, source, opts={}, &block) + opts = opts.deep_symbolize_keys + opts[:__expand] ||= __expand + ExpansionTreeVisitor.handle_array_expansion(attribute, source, opts, &block) + end + + helper_method :handle_expansion, :handle_array_expansion, :build_json_expansion_path_tree + end + + def self.build_json_expansion_path_tree(*paths) + request = ExpansionTree.new + paths = paths.flatten + if paths.count == 1 + if paths.first.is_a? String + request = ExpansionTree.new(*paths) + elsif paths.first.is_a? ExpansionTree + request = paths.first + end + elsif paths.any? + request = ExpansionTree.new(*paths) + end + + request + end + + # Applies Visitor(ish) pattern to an object. One of these is created for every new partial corresponding to a node in the tree + # + # Should not be used directly. + # + # @api private + class ExpansionTreeVisitor + # the attribute to add to the outputted JSON + # @return [Symbol] + attr_reader :attribute + + # the object we're visiting + attr_reader :object + + # @param [Symbol] attribute the attribute in the outputted JSON + # @param [Object,Enumerable] object the object being visited. object is an enumerable, then it's an array + # @option opts [JbuilderTemplate] :json the JBuilder object being used to generate the outputted JSON This is required. + # @option opts [ExpansionTree] :__expand the ExpansionTree for this current template. This is required. + # @option opts [Symbol] :as the method we call on 'object' when expanding the node. + # @option opts [Symbol] :item_as the method we call on each of the array items as part of a #handle_array_expansion call. + def initialize(attribute, object, opts) + @attribute = attribute + @object = object + @opts = opts.deep_symbolize_keys + end + + # the JBuilder object being used to generate the outputted JSON + # @return [JbuilderTemplate] the JBuilder object being used to generate the outputted JSON + def json + @opts[:json] + end + + # the ExpansionTree for the current partial + # @return [ExpansionTree] the tree for the current partial + def exp_request + @opts[:__expand] + end + + # the method we call on 'object' when expanding the node. Defaults to #attribute + # @return [Symbol] + def as + @opts[:as] || attribute + end + + # the method we call on each of the array items when expanding the node as part of a #handle_array_expansion call. Defaults to #attribute + # @return [Symbol] + def item_as + @opts[:item_as] || attribute + end + + def visit_expansion + if object.nil? + json.set! attribute, nil + else + if exp_request.expand? attribute + json.set! attribute do + json.partial! object, as: as, __expand: exp_request[attribute] + end + else + json.set! attribute, object&.to_houid + end + end + end + + # If #attribute is not set to expand then create an array of houids from the item in #object like: + # + # `json.friends ['friend_3254n132', 'friend_1245']` + # + # If #attribute is set to expand then create an array with the objects associated with the items. Alternatively, you + # can pass a block and this will yield an {ItemExpansion} so you can do your own manipulation of the specific item. + def visit_array_expansion + json.set! attribute do + if !exp_request.expand? attribute + json.array! object.map(&:to_houid) + else + object.each do |item| + json.child! do + if block_given? + yield(ItemExpansion.new(item, item_as, {json: json, __expand: exp_request[attribute]})) + else + ItemExpansion.new(item, item_as, {json: json, __expand: exp_request[attribute]}).handle_item_expansion + end + end + end + end + end + end + + def visit_item_expansion + json.partial! object, as: as, __expand: exp_request + end + + def self.handle_expansion(attribute, object, opts) + ExpansionTreeVisitor.new(attribute, object, opts).visit_expansion + end + + def self.handle_array_expansion(attribute, object, opts, &block) + ExpansionTreeVisitor.new(attribute, object, opts).visit_array_expansion(&block) + end + end + + class ItemExpansion + attr_reader :item, :as + + + def initialize(item, as, opts={}) + @item = item + @as = as + @json = opts[:json] + @__expand = opts[:__expand] + end + + def handle_item_expansion(object=nil) + if object.nil? + object = item + end + ExpansionTreeVisitor.new(nil, object, {as: as, json: @json, __expand: @__expand}).visit_item_expansion + end + end + + # an ExpansionTree takes a list of JSON SPath and then generates a tree for calculating what should be expanded + # To help understand why this a tree, consider the following SPaths: `supporter`, `nonprofit`, `nonprofit.user`. + # One could describe the SPath expansions as follows: + # + # ```ruby + # (root) + # | + # |---supporter + # | + # |---nonprofit + # | + # |--- user + # ``` + # + # @note You shouldn't be using the details of this class directly, you'll just be passing the object around + # as part of the various expansion requests + # @api private + class ExpansionTree + attr_accessor :root_node + + # @param [Array] paths the JSON SPaths for this tree + def initialize(*paths) + @root_node = Node.new + parse_paths(paths) + end + + # @param [string] path the path location to get the {ExpansionTree} subtree + # @return [ExpansionTree] the {ExpansionTree} at that location + def [](path) + unless @root_node.leaf? + er = ExpansionTree.create_from(@root_node[path] || Node.new) + er + else + ExpansionTree.new + end + end + + # + def expand?(path) + @root_node.has_key?(path) + end + + + # a node in the ExpansionTree + class Node < ActiveSupport::HashWithIndifferentAccess + # @return [boolean] true if this is a leaf node, false otherwise + def leaf? + none? + end + end + + private + + # given a set of SPaths, build a tree to describe + def parse_paths(paths=[]) + paths.each do |path| + working_tree = @root_node + path.split('.').each do |path_part| + working_tree[path_part] = Node.new unless working_tree[path_part] + working_tree = working_tree[path_part] + end + end + end + + def self.create_from(root_tree_node) + er = ExpansionTree.new() + er.root_node = root_tree_node + er + end + + end +end \ No newline at end of file diff --git a/app/controllers/concerns/controllers/api_new/nonprofit/current.rb b/app/controllers/concerns/controllers/api_new/nonprofit/current.rb new file mode 100644 index 000000000..344f2ef65 --- /dev/null +++ b/app/controllers/concerns/controllers/api_new/nonprofit/current.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + + +# You need this is in (ApiController)s. Eventually it should be used for all controllers but we're not there yet. +module Controllers::ApiNew::Nonprofit::Current + extend ActiveSupport::Concern + included do + private + + def current_nonprofit + result = Nonprofit.find_by(houid:params[:nonprofit_id]) + if Rails.version < '5' && result.nil? + raise ActiveRecord::RecordNotFound.new + end + + result + end + + def current_nonprofit_without_exception + begin + current_nonprofit + rescue + false + end + end + end +end diff --git a/app/controllers/concerns/controllers/api_new/transaction/current.rb b/app/controllers/concerns/controllers/api_new/transaction/current.rb new file mode 100644 index 000000000..7818f9513 --- /dev/null +++ b/app/controllers/concerns/controllers/api_new/transaction/current.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Controllers::ApiNew::Transaction::Current + extend ActiveSupport::Concern + include Controllers::ApiNew::Nonprofit::Current + + included do + private + + def current_transaction + result = @current_transaction + if result.nil? + result = current_nonprofit.transactions.find_by(houid:params[:transaction_id] || params[:id]) + if Rails.version < '5' && result.nil? + raise ActiveRecord::RecordNotFound + end + end + @current_transaction = result + end + end +end diff --git a/app/controllers/concerns/controllers/locale.rb b/app/controllers/concerns/controllers/locale.rb new file mode 100644 index 000000000..7a20defc8 --- /dev/null +++ b/app/controllers/concerns/controllers/locale.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +# rubocop:disable Style/ConditionalAssignment +module Controllers::Locale + extend ActiveSupport::Concern + + included do + around_action :switch_locale + + private + + def switch_locale(&action) + locale =if available_locales.include?(params[:locale]) + params[:locale] + else + extract_locale_from_accept_language_header + end + + logger.debug "* Locale set to '#{locale}'" + I18n.with_locale(locale, &action) + end + + def extract_locale_from_accept_language_header + # override compared to Houdini because we don't have bess yet + Settings.language + end + + def available_locales + # we don't have bess so override + Settings.available_locales.map { |locale| locale.to_s } + end + end +end +# rubocop:enable all diff --git a/app/controllers/concerns/controllers/nonprofit/authorization.rb b/app/controllers/concerns/controllers/nonprofit/authorization.rb new file mode 100644 index 000000000..c65035b52 --- /dev/null +++ b/app/controllers/concerns/controllers/nonprofit/authorization.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Controllers::Nonprofit::Authorization + extend ActiveSupport::Concern + include Controllers::User::Authorization + + included do + helper_method :current_nonprofit_user? + + private + + def authenticate_nonprofit_user! + reject_with_sign_in unless current_nonprofit_user? + end + + def authenticate_nonprofit_admin! + reject_with_sign_in unless current_nonprofit_admin? + end + + def current_nonprofit_user? + return false if params[:preview] + return false unless current_nonprofit_without_exception + + @current_nonprofit_user ||= 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_nonprofit_admin ||= current_role?(:nonprofit_admin, current_nonprofit.id) || current_role?(:super_admin) + end + end +end diff --git a/app/controllers/concerns/controllers/user/authorization.rb b/app/controllers/concerns/controllers/user/authorization.rb new file mode 100644 index 000000000..9267f7ecc --- /dev/null +++ b/app/controllers/concerns/controllers/user/authorization.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Controllers::User::Authorization + extend ActiveSupport::Concern + + # rubocop:disable Metrics/BlockLength + # rubocop:disable Layout/LineLength + included do + helper_method :current_role?, :administered_nonprofit + + protected + + def authenticate_user! + reject_with_sign_in unless current_user + end + + def reject_with_sign_in(msg = nil) + respond_to do |format| + format.json { raise AuthenticationError } + format.any { block_with_sign_in(msg) } + end + end + + def block_with_sign_in(msg = nil) + if current_user + redirect_to root_path(redirect_url: request.fullpath) + else + redirect_to new_user_session_path(redirect_url: request.fullpath), flash: { error: msg } + end + end + + def current_role?(role_names, host_id = nil) + return false unless current_user + + role_names = Array(role_names) + QueryRoles.user_has_role?(current_user.id, role_names, host_id) + end + + def authenticate_confirmed_user!(msg = nil) + if !current_user + reject_with_sign_in(msg) + elsif !current_user.confirmed? && !current_role?(%i[super_associate super_admin]) + respond_to do |format| + format.json { raise AuthenticationError } + format.any { redirect_to new_user_confirmation_path, flash: { error: 'You need to confirm your account to do that.' } } + end + end + end + + def authenticate_super_associate! + reject_with_sign_in 'Please login.' unless current_role?(:super_admin) || current_role?(:super_associate) + 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.*}] + + return if request.format.symbol == :json || no_redirects.map { |p| referrer.match(p) }.any? + + session[:previous_url] = referrer + end + + def administered_nonprofit + return nil unless current_user + + ::Nonprofit.where(id: QueryRoles.host_ids(current_user_id, %i[nonprofit_admin nonprofit_associate])).last + end + + def current_user_id + current_user&.id + end + end +end + +# rubocop:enable all diff --git a/app/controllers/concerns/controllers/x_frame.rb b/app/controllers/concerns/controllers/x_frame.rb new file mode 100644 index 000000000..3f29ac0d3 --- /dev/null +++ b/app/controllers/concerns/controllers/x_frame.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/master/LICENSE +module Controllers::XFrame + extend ActiveSupport::Concern + + included do + private + + # allows the page to be put in a frame, i.e. remove the X-Frame-Options header + def allow_framing + response.headers.delete('X-Frame-Options') if response.headers.has_key?('X-Frame-Options') + response.headers.delete('x-frame-options') if response.headers.has_key?('x-frame-options') + end + + def prevent_framing(value="SAMEORIGIN") + response.headers['X-Frame-Options'] = value + 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 5eaf43fa7..1c94c8b21 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 7ece89dab..60c2b324a 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 21a4bbf57..167637c27 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 6710adcaf..e3954431f 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 @@ -74,7 +74,7 @@ def stats end def name_and_id - render json: QueryEvents.name_and_id(current_nonprofit.id) + @events = current_nonprofit.events.not_deleted.order("events.name ASC") end diff --git a/app/controllers/image_attachments_controller.rb b/app/controllers/image_attachments_controller.rb index aa378f5db..9f47cbbd6 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 8fe388196..480f3b930 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 @@ -25,7 +25,7 @@ def all_npo_supporters # used on supporter dashboard def specific_npo_supporters - supporter_ids = params['supporter_ids'].split(",").map { |s| s.to_i } + 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 diff --git a/app/controllers/nonprofits/activities_controller.rb b/app/controllers/nonprofits/activities_controller.rb index 6824e8a77..87696c3aa 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 c68ef006f..9073c33db 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 83a6765e2..49107cfc3 100644 --- a/app/controllers/nonprofits/button_controller.rb +++ b/app/controllers/nonprofits/button_controller.rb @@ -4,11 +4,11 @@ class ButtonController < ApplicationController include Controllers::NonprofitHelper - before_filter :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 + NonprofitMailer.delay.button_code(current_nonprofit, params[:to_email], params[:to_name], params[:from_email], params[:message], params[:code]) render json: {}, status: 200 end diff --git a/app/controllers/nonprofits/cards_controller.rb b/app/controllers/nonprofits/cards_controller.rb index fc0e377b5..38682f19a 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 @@ -20,7 +20,6 @@ def create requires(:holder_type).one_of('Supporter', 'Nonprofit') end end.when_valid do |d| - UpdateBillingSubscriptions.activate_from_trial(d[:nonprofit_id]) InsertCard.with_stripe(d[:card]) end ) diff --git a/app/controllers/nonprofits/charges_controller.rb b/app/controllers/nonprofits/charges_controller.rb index e3d87a6c3..25ee872d2 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 9c1a7fe2b..87a67860c 100644 --- a/app/controllers/nonprofits/custom_field_joins_controller.rb +++ b/app/controllers/nonprofits/custom_field_joins_controller.rb @@ -3,12 +3,12 @@ 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 .supporters.find(params[:supporter_id]) - .custom_field_joins + .custom_field_joins.where("custom_field_master_id IN (SELECT id from custom_field_masters WHERE custom_field_masters.nonprofit_id = ?)", current_nonprofit.id) .order('created_at DESC') end diff --git a/app/controllers/nonprofits/custom_field_masters_controller.rb b/app/controllers/nonprofits/custom_field_masters_controller.rb index f4e75a144..17fad57b3 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 4599788b9..7c8125228 100644 --- a/app/controllers/nonprofits/donations_controller.rb +++ b/app/controllers/nonprofits/donations_controller.rb @@ -3,8 +3,9 @@ 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] + before_action :reject_for_deactivated_nonprofits, only: [:create] # get /nonprofit/:nonprofit_id/donations def index @@ -15,7 +16,9 @@ def index def create if params[:token] - params[:donation][:token] = params[:token] + params[:donation][:fee_covered] = params[:fee_covered] + + 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| diff --git a/app/controllers/nonprofits/email_lists_controller.rb b/app/controllers/nonprofits/email_lists_controller.rb index 1f3a5af0c..25a0d8fbc 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 b942bfc00..9498680a8 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 e0e7df418..639c4bc03 100644 --- a/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb +++ b/app/controllers/nonprofits/miscellaneous_np_infos_controller.rb @@ -4,7 +4,7 @@ class MiscellaneousNpInfosController < ApplicationController 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 efde59505..7a247fe52 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 62a96bc5e..430093b05 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 @@ -65,13 +65,27 @@ def destroy # post /nonprofits/:nonprofit_id/payments/:id/resend_donor_receipt def resend_donor_receipt - PaymentMailer.resend_donor_receipt(params[:id]) + payment = Payment.find(params[:id]) + if payment.kind == 'Donation' || payment.kind == 'RecurringDonation' + JobQueue.queue(JobTypes::DonorPaymentNotificationJob, payment.donation.id, payment.id) + elsif payment.kind == 'Ticket' + TicketMailer.followup(payment.tickets.pluck(:id), payment.charge.id).deliver + elsif payment.kind == 'Refund' + Delayed::Job.enqueue JobTypes::DonorRefundNotificationJob.new(payment.refund.id) + end 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) + payment = Payment.find(params[:id]) + if payment.kind == 'Donation' || payment.kind == 'RecurringDonation' + JobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, payment.donation.id, payment.id, current_user.id) + elsif payment.kind == 'Ticket' + JobQueue.queue(JobTypes::TicketMailerReceiptAdminJob, payment.tickets.pluck(:id), current_user.id) + elsif payment.kind == 'Refund' + Delayed::Job.enqueue JobTypes::NonprofitRefundNotificationJob.new(payment.refund.id, current_user.id) + end render json: {} end end # class PaymentsController diff --git a/app/controllers/nonprofits/payouts_controller.rb b/app/controllers/nonprofits/payouts_controller.rb index 5b0e0b55b..97dae43cf 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, { @@ -27,9 +27,15 @@ 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 + @available_gross = balances['available']['gross'] + @available_net = balances['available']['net'] + @pending_net = balances['pending']['net'] + @can_make_payouts = @nonprofit.can_make_payouts? + @verification_status = @nonprofit&.stripe_account&.verification_status || :unverified + + @deadline = @nonprofit&.stripe_account&.deadline && @nonprofit.stripe_account.deadline.in_time_zone(@nonprofit.timezone).strftime('%B %e, %Y at %l:%M:%S %p') + + @steps_to_payout = @nonprofit.steps_to_payout end # get /nonprofits/:nonprofit_id/payouts/:id diff --git a/app/controllers/nonprofits/recurring_donations_controller.rb b/app/controllers/nonprofits/recurring_donations_controller.rb index 9aa4260e2..8f83892dc 100644 --- a/app/controllers/nonprofits/recurring_donations_controller.rb +++ b/app/controllers/nonprofits/recurring_donations_controller.rb @@ -3,7 +3,8 @@ module Nonprofits class RecurringDonationsController < ApplicationController include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user!, except: [:create] + before_action :authenticate_nonprofit_user!, except: [:create] + before_action :reject_for_deactivated_nonprofits, only: [:create] # get /nonprofits/:nonprofit_id/recurring_donations def index @@ -16,7 +17,7 @@ def index #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.merge(end_date_gt_or_equal: Time.current)) end end end @@ -31,15 +32,15 @@ def export 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]) + [:active_and_not_failed, :active, :failed, :fulfilled].each do |k| + if params.key?(k) + params[k] = params[k] == "true" end end params[:root_url] = root_url - ExportRecurringDonations::initiate_export(@nonprofit.id, params, current_user.id) + ExportRecurringDonations::initiate_export(@nonprofit.id, params, [current_user.id]) rescue => e e end diff --git a/app/controllers/nonprofits/refunds_controller.rb b/app/controllers/nonprofits/refunds_controller.rb index aecd77a5e..870833500 100644 --- a/app/controllers/nonprofits/refunds_controller.rb +++ b/app/controllers/nonprofits/refunds_controller.rb @@ -3,11 +3,11 @@ 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 - charge = Qx.select("*").from("charges").where(id: params[:charge_id]).execute.first + charge = current_nonprofit.charges.find(params[:charge_id]) params[:refund][:user_id] = current_user.id render_json{ InsertRefunds.with_stripe(charge, params['refund']) } end diff --git a/app/controllers/nonprofits/reports_controller.rb b/app/controllers/nonprofits/reports_controller.rb index fd7e03467..9dd6ca5cc 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/stripe_accounts_controller.rb b/app/controllers/nonprofits/stripe_accounts_controller.rb new file mode 100644 index 000000000..86515d5c2 --- /dev/null +++ b/app/controllers/nonprofits/stripe_accounts_controller.rb @@ -0,0 +1,70 @@ +module Nonprofits + class StripeAccountsController < ApplicationController + include Controllers::NonprofitHelper + before_action :authenticate_nonprofit_admin! + + layout 'layouts/apified' + + def index + render_json do + + raise ActiveRecord::RecordNotFound unless current_nonprofit.stripe_account + + current_nonprofit.stripe_account.to_json( except: [:object, :id, :created_at, :updated_at], methods: [:verification_status, :deadline]) + end + end + + # this is the start page when someone needs to verify their nonprofit + def verification + @theme = 'minimal' + @current_nonprofit = current_nonprofit + end + + # html page where we check repeatedly whether we received a verification update + def confirm + @theme = 'minimal' + @current_nonprofit = current_nonprofit + end + + def begin_verification + stripe_account_for_nonprofit = StripeAccountUtils.find_or_create(current_nonprofit.id) + current_nonprofit.reload + + status = NonprofitVerificationProcessStatus.where('stripe_account_id = ?', current_nonprofit.stripe_account_id).first + unless status + status = NonprofitVerificationProcessStatus.new(stripe_account_id: current_nonprofit.stripe_account_id) + end + + unless status.started_at + status.started_at = DateTime.now + end + + status.save! + + render json:{}, status: :ok + end + + # html page when a link failed + def retry + @theme = 'minimal' + @current_nonprofit = current_nonprofit + end + + def account_link + stripe_account_for_nonprofit = StripeAccountUtils.find_or_create(current_nonprofit.id) + current_nonprofit.reload + + if (current_nonprofit.stripe_account_id) + render json: Stripe::AccountLink.create({ + account:current_nonprofit.stripe_account_id, + refresh_url: nonprofits_stripe_account_url(current_nonprofit.id, {return_location: params[:return_location]}), + return_url: confirm_nonprofits_stripe_account_url(current_nonprofit.id, {return_location: params[:return_location]}), + type: 'account_onboarding', + collect: 'eventually_due' + }).to_json, status: 200 + else + render json:{error: "No Stripe account could be found or created. Please contact support@commitchange.com for assistance."}, status: 400 + end + end + end +end diff --git a/app/controllers/nonprofits/supporter_emails_controller.rb b/app/controllers/nonprofits/supporter_emails_controller.rb deleted file mode 100644 index 724e53be6..000000000 --- a/app/controllers/nonprofits/supporter_emails_controller.rb +++ /dev/null @@ -1,32 +0,0 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module Nonprofits - class SupporterEmailsController < ApplicationController - include Controllers::NonprofitHelper - before_filter :authenticate_nonprofit_user! - - 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 - elsif params[:supporter_ids] - ids = params[:supporter_ids] - end - - if ids.nil? || ids.empty? - 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 - end - - def gmail - gmail = SupporterEmail.create params[:gmail] - InsertActivities.for_supporter_emails([gmail.id]) - render json: gmail - end - end -end - diff --git a/app/controllers/nonprofits/supporter_notes_controller.rb b/app/controllers/nonprofits/supporter_notes_controller.rb index 554e4827a..e6bc28b6e 100644 --- a/app/controllers/nonprofits/supporter_notes_controller.rb +++ b/app/controllers/nonprofits/supporter_notes_controller.rb @@ -3,12 +3,11 @@ 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 - params[:supporter_note][:user_id] ||= current_user && current_user.id - render_json{ InsertSupporterNotes.create([params[:supporter_note]]) } + render json: [Supporter.find(params[:supporter_id]).supporter_notes.create!(create_params.merge(user: current_user))] end # put /nonprofits/:nonprofit_id/supporters/:supporter_id/supporter_notes/:id @@ -23,5 +22,10 @@ def destroy render_json{ UpdateSupporterNotes.delete(params[:id]) } end + private + def create_params + params.require(:supporter_note).permit(:content) + + end end end diff --git a/app/controllers/nonprofits/supporters_controller.rb b/app/controllers/nonprofits/supporters_controller.rb index 380564ccb..639ee8622 100644 --- a/app/controllers/nonprofits/supporters_controller.rb +++ b/app/controllers/nonprofits/supporters_controller.rb @@ -3,9 +3,12 @@ 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 :validate_allowed!, only: [:create] + rescue_from ::TempBlockError, with: :handle_temp_block_error + + # get /nonprofit/:nonprofit_id/supporters def index @panels_layout = true @@ -51,11 +54,11 @@ def show end def email_address - render json: Supporter.find(params[:supporter_id]).email + render json: Supporter.find(params[:id]).email end def full_contact - fc = FullContactInfo.where("supporter_id=#{params[:supporter_id]}").first + fc = FullContactInfo.where("supporter_id= ?", params[:id]).first if fc render json: {full_contact: QueryFullContactInfos.fetch_associated_tables(fc.id )} else @@ -64,7 +67,7 @@ def full_contact end def info_card - render json: QuerySupporters.for_info_card(params[:supporter_id]) + render json: QuerySupporters.for_info_card(params[:id]) end @@ -106,6 +109,14 @@ def merge } end + def validate_allowed! + raise(TempBlockError) if must_block? + end + + def handle_temp_block_error + render json: {error: "no"}, status: :unprocessable_entity + end + # def new # @nonprofit = current_nonprofit # end diff --git a/app/controllers/nonprofits/tag_joins_controller.rb b/app/controllers/nonprofits/tag_joins_controller.rb index 2552120cd..92fa14b06 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 @@ -17,9 +17,9 @@ 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']} else - supporter_ids = params[:supporter_ids]. map(&:to_i) + 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, tag_modify_params) @@ -31,6 +31,17 @@ def destroy render json: {}, status: :ok end + + private + + def modify_params + params.permit(:selecting_all, query:[], supporter_ids:[], tags:[:tag_master_id, :selected]) + end + + def tag_modify_params + modify_params.require(:tags) + end + end end diff --git a/app/controllers/nonprofits/tag_masters_controller.rb b/app/controllers/nonprofits/tag_masters_controller.rb index cd0132da6..6abc8a7a6 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 d33e46a40..ccd1369f0 100755 --- a/app/controllers/nonprofits_controller.rb +++ b/app/controllers/nonprofits_controller.rb @@ -1,10 +1,15 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class NonprofitsController < ApplicationController include Controllers::NonprofitHelper + include Controllers::XFrame 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] + before_action :authenticate_super_admin!, only: [:destroy] + caches_action :btn + + after_action :allow_framing, only: [:donate, :btn] + # get /nonprofits/:id # get /:state_code/:city/:name @@ -35,7 +40,7 @@ def show end def recurring_donation_stats - render json: QueryRecurringDonations.overall_stats(params[:nonprofit_id]) + render json: QueryRecurringDonations.overall_stats(current_nonprofit.id) end def profile_todos @@ -54,11 +59,14 @@ def create def update flash[:notice] = 'Update successful!' current_nonprofit.update_attributes params[:nonprofit].except(:verification_status) + expire_action :action => :btn + current_nonprofit.clear_cache json_saved current_nonprofit end - def destroy - current_nonprofit.destroy + def destroy + current_nonprofit.clear_cache + current_nonprofit.destroy flash[:notice] = 'Nonprofit removed' render json: {} end @@ -74,7 +82,7 @@ def donate def btn @nonprofit = current_nonprofit - respond_to { |format| format.html{render layout: 'layouts/embed'} } + respond_to { |format| format.html{render layout: 'layouts/btn'} } end # get /nonprofits/:id/supporter_form @@ -90,7 +98,11 @@ def custom_supporter end def dashboard - @nonprofit = current_nonprofit + @nonprofit = current_nonprofit + @can_make_payouts = @nonprofit.can_make_payouts? + @verification_status = @nonprofit&.stripe_account&.verification_status || :unverified + + @deadline = @nonprofit&.stripe_account&.deadline && @nonprofit.stripe_account.deadline.in_time_zone(@nonprofit.timezone).strftime('%B %e, %Y at %l:%M:%S %p') respond_to { |format| format.html } end @@ -102,18 +114,6 @@ def payment_history render json: NonprofitMetrics.payment_history(params) end - # put /nonprofits/:id/verify_identity - def verify_identity - if params[:legal_entity][:address] - tos = { - ip: current_user.current_sign_in_ip, - date: Time.current.to_i, - user_agent: request.user_agent - } - 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]) end diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb index d219c4e20..0f01490c7 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 @@ -11,9 +11,9 @@ 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 + if @profile.anonymous? && current_user_id != @profile.user_id && !current_role?(:super_admin) flash[:notice] = 'That user does not have a public profile.' - redirect_to(request.env["HTTP_REFERER"] || root_url) + redirect_to(root_url) return end end diff --git a/app/controllers/recurring_donations_controller.rb b/app/controllers/recurring_donations_controller.rb index 3561dd65c..f8bfd5b28 100644 --- a/app/controllers/recurring_donations_controller.rb +++ b/app/controllers/recurring_donations_controller.rb @@ -3,7 +3,9 @@ class RecurringDonationsController < ApplicationController def edit @data = QueryRecurringDonations.fetch_for_edit params[:id] + if @data && params[:t] == @data['recurring_donation']['edit_token'] + @nonprofit = RecurringDonation.find(params[:id]).nonprofit @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? @@ -45,8 +47,8 @@ def update_amount 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], params[:fee_covered]) + flash[:notice] = "Your recurring donation amount has been successfully changed to #{print_currency(amount_response.amount, '$')}" render_json { amount_response } rescue => e render_json { raise e } @@ -56,4 +58,14 @@ def update_amount end end + private + + def print_currency(cents, unit="EUR", sign=true) + + dollars = cents.to_f / 100.0 + dollars = view_context.number_to_currency(dollars, :unit => "#{unit}", :precision => (dollars.round == dollars) ? 0 : 2) + dollars = dollars[1..-1] if !sign + dollars + end + end diff --git a/app/controllers/roles_controller.rb b/app/controllers/roles_controller.rb index 5dbe31ea7..ff211e3b3 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 da64cf2b2..0ee1df012 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] @@ -24,6 +24,8 @@ def index if @nonprofit @miscellaneous_np_info = FetchMiscellaneousNpInfo.fetch(@nonprofit.id) + + @steps_to_payout = @nonprofit.steps_to_payout end end diff --git a/app/controllers/super_admins_controller.rb b/app/controllers/super_admins_controller.rb index 3d20b6365..84ed07a56 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 e0082a0ee..3e12ff56e 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 1a8b15692..407961b04 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 diff --git a/app/controllers/users/confirmations_controller.rb b/app/controllers/users/confirmations_controller.rb index 97812ee84..546aec90b 100644 --- a/app/controllers/users/confirmations_controller.rb +++ b/app/controllers/users/confirmations_controller.rb @@ -1,6 +1,5 @@ # 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]) diff --git a/app/controllers/users/registrations_controller.rb b/app/controllers/users/registrations_controller.rb index 24288ad80..49723f1b0 100644 --- a/app/controllers/users/registrations_controller.rb +++ b/app/controllers/users/registrations_controller.rb @@ -1,6 +1,10 @@ # 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 + + before_action :verify_via_recaptcha!, only: [:create] + + rescue_from ::Recaptcha::RecaptchaError, with: :handle_recaptcha_failure def new super @@ -8,8 +12,7 @@ def new # this endpoint only creates donor users def create - params[:user][:referer] = session[:referer_id] - user = User.register_donor!(params[:user]) + user = User.register_donor!({referer: session[:referer_id]}.merge(params[:user]) ) if user.save sign_in user render :json => user @@ -49,4 +52,28 @@ def update render :json => {:errors => errs}, :status => :unprocessable_entity end end + + + private + def verify_via_recaptcha! + begin + verify_recaptcha!(action: 'create_user', minimum_score: ENV['MINIMUM_RECAPTCHA_SCORE'].to_f) + rescue ::Recaptcha::RecaptchaError => e + failure_details = { + params: params, + action: 'create_user', + minimum_score_required: ENV['MINIMUM_RECAPTCHA_SCORE'], + recaptcha_result: recaptcha_reply, + recaptcha_value: params['g-recaptcha-response'] + } + failure = RecaptchaRejection.new + failure.details = failure_details + failure.save! + raise e + end + end + + def handle_recaptcha_failure + render json: {error: "There was an temporary error preventing your payment. Please try again. If it persists, please contact support@commitchange.com with error code: 5X4J "}, status: :unprocessable_entity + end end diff --git a/app/controllers/users/sessions_controller.rb b/app/controllers/users/sessions_controller.rb index d9a4ad100..c8e101cfe 100644 --- a/app/controllers/users/sessions_controller.rb +++ b/app/controllers/users/sessions_controller.rb @@ -1,9 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later class Users::SessionsController < Devise::SessionsController + include ::Controllers::XFrame + layout 'layouts/apified', only: :new - - - def new + + after_action :prevent_framing + + def new @theme = 'minimal' super end @@ -13,7 +16,8 @@ def create respond_to do |format| format.json { - warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new") + self.resource = warden.authenticate!(:scope => resource_name, :recall => "#{controller_path}#new") + sign_in(resource_name, resource) render :status => 200, :json => { :status => "Success" } } end @@ -29,7 +33,7 @@ def confirm_auth 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 + render json: ["Incorrect password. Please enter your #{Settings.general.name} password."], status: :unprocessable_entity end end diff --git a/app/controllers/webhooks/stripe_controller.rb b/app/controllers/webhooks/stripe_controller.rb new file mode 100644 index 000000000..59d24b99c --- /dev/null +++ b/app/controllers/webhooks/stripe_controller.rb @@ -0,0 +1,54 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module Webhooks + class StripeController < ApplicationController + rescue_from Exception, with: :unspecified_error + rescue_from JSON::ParserError, with: :invalid_payload_error + rescue_from Stripe::SignatureVerificationError, with: :signature_invalid_error + + def receive + sig_header = request.headers['HTTP_STRIPE_SIGNATURE'] + event = nil + payload = JSON.parse(request.raw_post) + event = Stripe::Webhook.construct_event( + request.raw_post, sig_header, ENV['STRIPE_WEBHOOK_SECRET'] + ) + + if ENV["RAILS_ENV"] != "production" || event.livemode + StripeEvent.handle(event) + end + render json:{}, status: 200 + end + + def receive_connect + sig_header = request.headers['HTTP_STRIPE_SIGNATURE'] + event = nil + payload = JSON.parse(request.raw_post) + event = Stripe::Webhook.construct_event( + request.raw_post, sig_header, ENV['STRIPE_CONNECT_WEBHOOK_SECRET'] + ) + + if ENV["RAILS_ENV"] != "production" || event.livemode + StripeEvent.handle(event) + end + render json:{}, status: 200 + end + + + + protected + def invalid_payload_error(exception) + render json: {error: "Invalid payload", details: exception.to_s, backtrace: exception.backtrace}, status: 400 + end + + def signature_invalid_error(exception) + render json: {error: "Invalid signature", details: exception.to_s, backtrace: exception.backtrace}, status: 400 + end + + def unspecified_error(exception) + if ENV["RAILS_ENV"] == "development" + raise exception + end + render json: {error: "Unspecified error", details: exception.to_s, backtrace: exception.backtrace}, status: 400 + end + end +end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index e2f981df7..0c54eac1a 100755 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -33,12 +33,6 @@ def simple_date date_object, timezone=nil 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 readable_date date_object date_object.strftime("%B %d, %Y") end @@ -52,11 +46,6 @@ 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 - # 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 diff --git a/app/helpers/card_helper.rb b/app/helpers/card_helper.rb index 7a9309e69..8fb509ab8 100644 --- a/app/helpers/card_helper.rb +++ b/app/helpers/card_helper.rb @@ -1,22 +1,6 @@ # 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 current_card - current_user && current_user.profile.card - end - def expiration_years (0..15).map{|n| (Date.today + n.years).year} end diff --git a/app/helpers/nonprofits_helper.rb b/app/helpers/nonprofits_helper.rb deleted file mode 100644 index 5d9ea4419..000000000 --- a/app/helpers/nonprofits_helper.rb +++ /dev/null @@ -1,16 +0,0 @@ -# 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 - -end diff --git a/app/helpers/onboard_helper.rb b/app/helpers/onboard_helper.rb deleted file mode 100644 index 1cb832a5b..000000000 --- a/app/helpers/onboard_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module OnboardHelper -end diff --git a/app/helpers/pricing_helper.rb b/app/helpers/pricing_helper.rb deleted file mode 100644 index db1d7b484..000000000 --- a/app/helpers/pricing_helper.rb +++ /dev/null @@ -1,8 +0,0 @@ -# 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 -end diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb deleted file mode 100755 index e40303e48..000000000 --- a/app/helpers/profiles_helper.rb +++ /dev/null @@ -1,12 +0,0 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -module ProfilesHelper - - def get_shortened_name name - if name - name.length > 18 ? name[0..18] + '...' : name - else - 'Your Account' - end - end - -end diff --git a/app/javascript/api/mocks/index.ts b/app/javascript/api/mocks/index.ts new file mode 100644 index 000000000..21d90fce6 --- /dev/null +++ b/app/javascript/api/mocks/index.ts @@ -0,0 +1,4 @@ +// License: LGPL-3.0-or-later +import { setupServer } from "msw/node"; + +export const server = setupServer(); \ No newline at end of file diff --git a/app/javascript/common/Callbacks/Callback.ts b/app/javascript/common/Callbacks/Callback.ts new file mode 100644 index 000000000..9682500a5 --- /dev/null +++ b/app/javascript/common/Callbacks/Callback.ts @@ -0,0 +1,36 @@ +// License: LGPL-3.0-or-later + +/** + * Describes an callback for some sort of change in an object. Kind of similar ActiveSupport::Callbacks + */ +export default class Callback { + constructor(public readonly props: T) { + + } + + /** + * A boolean method deciding whether .run should be called. By default it's true but can be overriden in subclasses. + * @returns true if this callback should be run, false if it should not. + */ + canRun(): boolean { + return true; + } + + /** + * Catches any errors thrown when running .run. By default, this simply rethrows the error. + * Child classes could use this to suppress some errors or rethrow a different error + * + * @param e the error caught + */ + catchError(e: unknown) { + throw e; + } + + /** + * Runs the callback itself. Must be implemented in a child class. + * @return a Promise or void + */ + run(): Promise | void { + throw new Error("You need to implement 'run' in your child class"); + } +} \ No newline at end of file diff --git a/app/javascript/common/Callbacks/CallbackController.spec.ts b/app/javascript/common/Callbacks/CallbackController.spec.ts new file mode 100644 index 000000000..f8bd2bdb9 --- /dev/null +++ b/app/javascript/common/Callbacks/CallbackController.spec.ts @@ -0,0 +1,145 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +// License: LGPL-3.0-or-later + +import Callback from "./Callback"; +import CallbackControllerBuilder from "./CallbackControllerBuilder"; + +jest.mock('./run', () => { + return jest.fn(); +}); + +import run from './run'; + +const runMocked = run as jest.Mock; + +describe('Runner', () => { + + + beforeEach(() => { + runMocked.mockClear(); + }); + + interface CustomInputType { + field: string; + } + + class CallbackClass1 extends Callback { + + } + + class CallbackClass2 extends Callback { + + } + + class CallbackClass3 extends Callback { + + } + + function buildRunner() { + return new CallbackControllerBuilder('success', 'failure').withInputType(); + } + + function buildRunnerWithBeforeSuccessAndAfterForBoth() { + const runner = new CallbackControllerBuilder('success', 'failure').withInputType(); + runner.addAfterCallback('success', CallbackClass1); + runner.addAfterCallback('failure', CallbackClass2); + runner.addBeforeCallback('success', CallbackClass3); + return runner; + } + + describe('.callbacks', () => { + it('defaults to empty arrays for each callback type', () => { + const runner = buildRunner(); + expect(Array.from(runner.callbacks().entries())).toStrictEqual([ + ['success', {before: [], after:[]}], + ['failure', {before: [], after:[]}], + ]); + }); + + it('contains callbacks when added', () => { + const runner = buildRunnerWithBeforeSuccessAndAfterForBoth(); + expect(Array.from(runner.callbacks().entries())).toStrictEqual([ + ['success', {before: [CallbackClass3], after: [CallbackClass1]}], + ['failure', {before: [], after: [CallbackClass2]}], + ]); + }); + + it('returns Map when when no type passed', () => { + const runner = buildRunnerWithBeforeSuccessAndAfterForBoth(); + expect(runner.callbacks()).toBeInstanceOf(Map); + }); + + it('returns undefined when an invalid callback type is passed', () => { + const runner = buildRunner(); + expect(runner.callbacks('invalid' as any)).toBeUndefined(); + }); + + it('returns an array of callbacks when a valid callback type is passed', () => { + const runner = buildRunnerWithBeforeSuccessAndAfterForBoth(); + expect(runner.callbacks('success')).toBeInstanceOf(Object); + expect(runner.callbacks('success')?.after).toBeInstanceOf(Array); + expect(runner.callbacks('success')?.after).toStrictEqual([CallbackClass1]); + + expect(runner.callbacks('success')?.before).toBeInstanceOf(Array); + expect(runner.callbacks('success')?.before).toStrictEqual([CallbackClass3]); + }); + }); + + describe('.addCallback', () => { + it('defaults to empty arrays for each callback type', () => { + const runner = buildRunner(); + expect(Array.from(runner.callbacks().entries())).toStrictEqual([ + ['success', {before: [], after: []}], + ['failure', {before: [], after: []}], + ]); + }); + + it('contains callbacks when added', () => { + const runner = buildRunnerWithBeforeSuccessAndAfterForBoth(); + expect(Array.from(runner.callbacks().entries())).toStrictEqual([ + ['success', {after:[CallbackClass1], before: [CallbackClass3]}], + ['failure', {after:[CallbackClass2], before: []}], + ]); + }); + + it('orders callbacks in the order theyre received', () => { + const runner = buildRunner(); + runner.addAfterCallback('success', CallbackClass2); + runner.addAfterCallback('success', CallbackClass1); + + expect(runner.callbacks('success')?.after).toStrictEqual([CallbackClass2, CallbackClass1]); + }); + }); + + describe('.run', () => { + it('does not call run when an invalid callback type is used', async () => { + const runner = buildRunner(); + runner.addBeforeCallback('success', CallbackClass2); + runner.addAfterCallback('success', CallbackClass1); + + const actionFor = jest.fn(); + + await runner.run('fakeStatus' as any, { field: 'imaginary' }, actionFor); + + expect(runMocked).not.toHaveBeenCalled(); + expect(actionFor).not.toHaveBeenCalled(); + + }); + + it('calls run with proper information', async () => { + const runner = buildRunner(); + runner.addBeforeCallback('success', CallbackClass2); + runner.addAfterCallback('success', CallbackClass1); + const actionFor = jest.fn(); + await runner.run('success', { field: 'imaginary' }, actionFor); + + expect(runMocked).toHaveBeenCalledWith({ field: 'imaginary' }, [CallbackClass2]); + expect(runMocked).toHaveBeenLastCalledWith({field: 'imaginary'}, [CallbackClass1]); + expect(actionFor).toHaveBeenCalled(); + }); + }); + + + +}); + diff --git a/app/javascript/common/Callbacks/CallbackController.ts b/app/javascript/common/Callbacks/CallbackController.ts new file mode 100644 index 000000000..f1e30b317 --- /dev/null +++ b/app/javascript/common/Callbacks/CallbackController.ts @@ -0,0 +1,102 @@ +// License: LGPL-3.0-or-later +import run from './run'; +import { CallbackAccessor, CallbackClass, CallbackFilters, CallbackMap} from "./types"; +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- it's used in JSDoc +import type Callback from './Callback'; + +type ModifiableCallbackFilters = { after: CallbackClass[], before: CallbackClass[]}; + +type ModifiableCallbackMap = Map>>; + +/** + * Manages the callbacks for a set of actions described by a specific action name + * @template CallbackProps the type available in each {@link Callback} at {@link Callback.props} + * @template ActionNames the names of the various actions which callbacks will occur on. + * @hideConstructor + */ +export default class CallbackController< + // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-constraint + CallbackProps extends unknown, + ActionNames extends string, + > implements CallbackAccessor{ + + private _callbacks: ModifiableCallbackMap = new Map(); + + /** + * @param actionNames the names of all the actions whos callbacks are managed by this object + */ + constructor(actionNames: ActionNames[]) { + for (const actionName of actionNames) { + this._callbacks.set(actionName, {before: [], after:[]}); + } + + } + + /** + * Add a callback to be run after an action occurs + * @param callbackType a string representing the particular action to + * @param actions + */ + addAfterCallback(callbackType: ActionNames, ...actions: CallbackClass[]):void { + this.addCallback(callbackType, 'after', ...actions); + } + + addBeforeCallback(callbackType: ActionNames, ...actions: CallbackClass[]):void { + this.addCallback(callbackType, 'before', ...actions); + } + + callbacks(): CallbackMap; + callbacks(actionName: ActionNames): CallbackFilters> | undefined; + callbacks(actionName?: ActionNames): CallbackMap | CallbackFilters> | undefined { + if (actionName) { + return this._callbacks.has(actionName) ? this._callbacks.get(actionName) : undefined; + } + else { + return this._callbacks; + } + + } + + /** + * The names of actions managed by this controller + */ + get actionNames(): ActionNames[] { + return [...this._callbacks.keys()]; + } + + /** + * Runs the before callbacks for an action, the action itself and then the after callbacks for an action + * @param actionName the name of the action to run. If the name isn't in + * @param callbackProps the properties to pass each of the callbacks. + * @param action the action to run corresponding to the {@link actionName} + * @returns a promise to indicate when the last after callback has finished. + * @throws when one of the callbacks raises an exception and doesn't catch an exception or if {@link action} raises an + * exception + */ + async run(actionName: ActionNames, callbackProps: CallbackProps, action:()=> Promise|void): Promise { + const callbacks = this.callbacks(actionName); + if (callbacks) { + + await run(callbackProps, callbacks.before); + + await action(); + + await run(callbackProps, callbacks.after); + } + } + + private addCallback(callbackType: ActionNames, filter: 'after'|'before', ...actions: CallbackClass[]) : void { + const after_and_before = this._callbacks.get(callbackType); + if (after_and_before) { + if (filter === 'after') { + after_and_before.after = [...after_and_before.after, ...actions]; + } + else { + after_and_before.before = [...after_and_before.before, ...actions]; + } + } + } + +} + + diff --git a/app/javascript/common/Callbacks/CallbackControllerBuilder.ts b/app/javascript/common/Callbacks/CallbackControllerBuilder.ts new file mode 100644 index 000000000..ee6e95203 --- /dev/null +++ b/app/javascript/common/Callbacks/CallbackControllerBuilder.ts @@ -0,0 +1,71 @@ +// License: LGPL-3.0-or-later +import CallbackController from "./CallbackController"; + +/** + * Builds your {@link CallbackController} in a type-safe way + * + * @example Create a CallbackController to manage the callbacks for `validate` and `update` actions + * // the an object containing properties to pass into your callbacks + * type ResultHolder = {resultObject?: TResultObject} // TResultObject is an arbrary type + * type CallbackPropsType = { inputObject: {givenName:string, familyName: string}} + * + * const props: CallbackPropsType = { + * inputObject: {givenName: "Penelope", familyName: "Schultz"} + * result: {resultObject: null} + * } + * + * const controller = new CallbackControllerBuilder('create', 'update').withInputType(); + * + * controller.addBeforeCallback('validate', CleanupDataBeforeValidation) // CleanupDataBeforeValidation is a class you created + * controller.addBeforeCallback('validate', LogValidationAttempt) // LogValidationAttempt is a class you created + * controller.addBeforeCallback('update', PrepareForUpdate) // PrepareForUpdate is a class you created + * controller.addAfterCallback('update', LogAfterUpdate) // LogAfterUpdate is a class you created + * + * + * async function performUpdate(props:CallbackPropsType): Promise { + * controller.run('validate', props, () => { + * // run validation action + * }) + * + * controller.run('update', props, async () => { + * // run update to get a result Object + * const result:TResultObject = await update(props) + * props.result.resultObject = result + * }) + * } + * + * await performUpdate(props:CallbackPropsType) + * // This runs the following in order: + * // * before callbacks for the 'validate' action: + * // * CleanupDataBeforeValidation.run if CleanupDataBeforeValidation.canRun() is true + * // * LogValidationAttempt.run if LogValidationAttempt.canRun() is true + * // * the action passed into run with "validate" + * // * after callbacks for 'validate' action(but not are set so nothing to run) + * // * before callbacks for 'update' action + * // * PrepareForUpdate.run if PrepareForUpdate.canRun() is true + * // * the action passed into run with "update" + * // * after callbacks for 'update' action' + * // * LogAfterUpdate if LogAfterUpdate.canRun() is true + */ +export default class CallbackControllerBuilder { + private readonly actionNames: ActionNames[]; + + /** + * + * @param actionNames the various actions whose callbacks you want managed + */ + constructor(...actionNames: ActionNames[]) { + this.actionNames = actionNames; + } + + /** + * A helper method to safely type a {@link CallbackController}. You will need to explicitly set the + * {@link TInputProps} type. + * @template TInputProps the type of properties passed into every {@link Callback} created by the + * {@link CallbackController} + * @returns a strongly typed {@link CallbackController} + */ + withInputType(): CallbackController { + return new CallbackController(this.actionNames); + } +} diff --git a/app/javascript/common/Callbacks/index.ts b/app/javascript/common/Callbacks/index.ts new file mode 100644 index 000000000..81f54cd1c --- /dev/null +++ b/app/javascript/common/Callbacks/index.ts @@ -0,0 +1,5 @@ +// License: LGPL-3.0-or-later +import Callback from './Callback'; +import CallbackControllerBuilder from './CallbackControllerBuilder'; + +export {Callback, CallbackControllerBuilder}; \ No newline at end of file diff --git a/app/javascript/common/Callbacks/run.ts b/app/javascript/common/Callbacks/run.ts new file mode 100644 index 000000000..39199a71a --- /dev/null +++ b/app/javascript/common/Callbacks/run.ts @@ -0,0 +1,30 @@ +// License: LGPL-3.0-or-later +import type { CallbackClass } from "./types"; + +/** + * A very simple function for conditionally running callbacks. Move into own file because we can mock it for CallbackController + * @param input The input properties to every callback + * @param callbacks callbacks as classes to be run + * @template TCallbackProps the properties to be passed into the constructor of each Callback + * @returns {Promise} a promise resolving on completion of all of the callbacks. Doesn't currently ever reject. + */ +export default async function run(input: TCallbackProps, callbacks: readonly CallbackClass[]): Promise { + try { + for (const callback of callbacks) { + const obj = new callback(input); + + if (obj.canRun()) { + try { + await obj.run(); + } + catch (e) { + obj.catchError(e); + } + } + } + } + catch (e) { + console.error(`Runner failed with ${e}`); + } + +} \ No newline at end of file diff --git a/app/javascript/common/Callbacks/types.ts b/app/javascript/common/Callbacks/types.ts new file mode 100644 index 000000000..fc23f1786 --- /dev/null +++ b/app/javascript/common/Callbacks/types.ts @@ -0,0 +1,52 @@ +// License: LGPL-3.0-or-later +import Callback from "./Callback"; + +// eslint-disable-next-line @typescript-eslint/no-unused-vars -- used for doc +import type CallbackController from "./CallbackController"; + + +export type CallbackClass = typeof Callback; + + +/** + * Contains the {@link CallbackClass}'s to be run before or after an action occurs. These callback are run in order for + * each filter type + * @template TCallbackClass the superclass of all the Classes to be run. + */ + +export interface CallbackFilters { + + /** + * The callbacks to run before the action occurs. + */ + readonly after: readonly TCallbackClass[]; + + /** + * The callback to run after the action occurs + */ + readonly before: readonly TCallbackClass[]; +} + +export type CallbackMap = ReadonlyMap>>; + +/** + * Get callbacks handled by an object. {@link CallbackController} implements this but it's possible you might have a class that + * contains a {@link CallbackController} and implements this by proxying it to CallbackController. + * + * @template CallbackProps the type available in each {@link Callback} at {@link Callback.props} + * @template ActionNames the names of the various actions which callbacks will occur on. + * @hideConstructor + */ +export interface CallbackAccessor { + + /** + * All callbacks registered on an object. Either part of a controller but might also be + */ + callbacks(): CallbackMap; + callbacks(actionName: ActionNames): CallbackFilters> | undefined; + callbacks(actionName?: ActionNames): CallbackMap | CallbackFilters> | undefined; +} + + + diff --git a/app/jobs/active_recurring_donations_to_csv_job.rb b/app/jobs/active_recurring_donations_to_csv_job.rb new file mode 100644 index 000000000..45cda1b6f --- /dev/null +++ b/app/jobs/active_recurring_donations_to_csv_job.rb @@ -0,0 +1,10 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class ActiveRecurringDonationsToCsvJob < ExportJob + queue_as :default + + def perform(opts={}) + url = ExportRecurringDonations.run_export_for_active_recurring_donations_to_csv(opts[:nonprofit_s3_key], opts[:filename], opts[:export]) + export.update(url: url) + end + +end diff --git a/app/jobs/application_job.rb b/app/jobs/application_job.rb new file mode 100644 index 000000000..a6dd13698 --- /dev/null +++ b/app/jobs/application_job.rb @@ -0,0 +1,5 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# newer versions of Rails use an ApplicationJob so let's be cool like them +class ApplicationJob < ActiveJob::Base + queue_as :default +end \ No newline at end of file diff --git a/app/jobs/export_job.rb b/app/jobs/export_job.rb new file mode 100644 index 000000000..e8dbdec63 --- /dev/null +++ b/app/jobs/export_job.rb @@ -0,0 +1,43 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class ExportJob < ApplicationJob + queue_as :default + + before_enqueue do |job| + job.export = Export.create(nonprofit: job.nonprofit, user: job.user, status: :queued) + end + + before_perform do |job| + job.export = Export.create(nonprofit: job.nonprofit, user: job.user, status: :queued) unless export + job.export.update(status: :started) + end + + after_perform do |job| + job.export.update(status: :completed, ended:Time.current) + end + + rescue_from 'Exception' do |exception| + self.export.update(status: :failed, ended:Time.current, exception: exception.to_s) + raise exception + end + + protected + + # we use these where to get the args in various callbacks + def nonprofit + arguments.first[:nonprofit] + end + + def user + arguments.first[:user] + end + + def export + arguments.first[:export] + end + + def export=(export) + arguments.first[:export] = export + end + + +end diff --git a/app/jobs/inline_job.rb b/app/jobs/inline_job.rb new file mode 100644 index 000000000..9fb5b2f6f --- /dev/null +++ b/app/jobs/inline_job.rb @@ -0,0 +1,5 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# newer versions of Rails use an ApplicationJob so let's be cool like them +class InlineJob< ActiveJob::Base + queue_adapter = :inline +end \ No newline at end of file diff --git a/app/jobs/inline_job/modern_object_donation_stripe_charge_job.rb b/app/jobs/inline_job/modern_object_donation_stripe_charge_job.rb new file mode 100644 index 000000000..d8065e6dd --- /dev/null +++ b/app/jobs/inline_job/modern_object_donation_stripe_charge_job.rb @@ -0,0 +1,29 @@ +class InlineJob::ModernObjectDonationStripeChargeJob < InlineJob + queue_as :default + + def perform(donation:, legacy_payment:) + supporter = Supporter.find(donation.supporter_id) + trx = supporter.transactions.build(amount: legacy_payment.gross_amount, created: legacy_payment['date']) + + don = trx.donations.build(amount: legacy_payment.gross_amount, legacy_donation: donation) + + stripe_transaction_charge = SubtransactionPayment.new( + legacy_payment: legacy_payment, + paymentable: StripeTransactionCharge.new, + created: legacy_payment.date + ) + stripe_t = trx.build_subtransaction( + subtransactable: StripeTransaction.new(amount: legacy_payment.gross_amount), + subtransaction_payments:[ + stripe_transaction_charge + ] + ); + trx.save! + don.save! + stripe_t.save! + stripe_t.subtransaction_payments.each(&:publish_created) + #stripe_t.publish_created + don.publish_created + trx.publish_created + end +end diff --git a/app/jobs/mailchimp_nonprofit_user_add_job.rb b/app/jobs/mailchimp_nonprofit_user_add_job.rb new file mode 100644 index 000000000..c55c28365 --- /dev/null +++ b/app/jobs/mailchimp_nonprofit_user_add_job.rb @@ -0,0 +1,9 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +class MailchimpNonprofitUserAddJob < ActiveJob::Base + queue_as :default + + def perform(user, nonprofit) + Mailchimp.signup_nonprofit_user(DripEmailList.first, nonprofit, user) + end +end diff --git a/app/jobs/mailchimp_signup_job.rb b/app/jobs/mailchimp_signup_job.rb new file mode 100644 index 000000000..7ee89d520 --- /dev/null +++ b/app/jobs/mailchimp_signup_job.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class MailchimpSignupJob < ApplicationJob + queue_as :default + + def perform(supporter, mailchimp_list) + Mailchimp.signup(supporter, mailchimp_list) + end +end diff --git a/app/jobs/populate_list_job.rb b/app/jobs/populate_list_job.rb new file mode 100644 index 000000000..37f34e1a8 --- /dev/null +++ b/app/jobs/populate_list_job.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PopulateListJob < ApplicationJob + queue_as :default + + def perform(email_list) + email_list.populate_list + end +end diff --git a/app/jobs/recurring_donation_cancelled_job.rb b/app/jobs/recurring_donation_cancelled_job.rb new file mode 100644 index 000000000..cc47833a9 --- /dev/null +++ b/app/jobs/recurring_donation_cancelled_job.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class RecurringDonationCancelledJob < ApplicationJob + queue_as :default + + def perform(recurring_donation) + recurring_donation.supporter&.active_email_lists&.update_member_on_all_lists + end +end diff --git a/app/jobs/recurring_donation_created_job.rb b/app/jobs/recurring_donation_created_job.rb new file mode 100644 index 000000000..d68ec4341 --- /dev/null +++ b/app/jobs/recurring_donation_created_job.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class RecurringDonationCreatedJob < ApplicationJob + queue_as :default + + def perform(recurring_donation) + recurring_donation.supporter&.active_email_lists&.update_member_on_all_lists + end +end diff --git a/app/jobs/started_recurring_donations_to_csv_job.rb b/app/jobs/started_recurring_donations_to_csv_job.rb new file mode 100644 index 000000000..6cf626f12 --- /dev/null +++ b/app/jobs/started_recurring_donations_to_csv_job.rb @@ -0,0 +1,10 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StartedRecurringDonationsToCsvJob < ExportJob + queue_as :default + + def perform(opts={}) + url = ExportRecurringDonations.run_export_for_started_recurring_donations_to_csv(opts[:nonprofit_s3_key], opts[:filename], opts[:export]) + export.update(url: url) + end + +end diff --git a/lib/audit.rb b/app/legacy_lib/audit.rb similarity index 100% rename from lib/audit.rb rename to app/legacy_lib/audit.rb diff --git a/lib/errors/authentication_error.rb b/app/legacy_lib/authentication_error.rb similarity index 100% rename from lib/errors/authentication_error.rb rename to app/legacy_lib/authentication_error.rb diff --git a/lib/calculate/calculate_suggested_amounts.rb b/app/legacy_lib/calculate_suggested_amounts.rb similarity index 98% rename from lib/calculate/calculate_suggested_amounts.rb rename to app/legacy_lib/calculate_suggested_amounts.rb index d4af6fab0..210dfb6b7 100644 --- a/lib/calculate/calculate_suggested_amounts.rb +++ b/app/legacy_lib/calculate_suggested_amounts.rb @@ -1,5 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later require 'numeric' +require 'param_validation' module CalculateSuggestedAmounts MIN = 25 diff --git a/lib/cancel_billing_subscription.rb b/app/legacy_lib/cancel_billing_subscription.rb similarity index 96% rename from lib/cancel_billing_subscription.rb rename to app/legacy_lib/cancel_billing_subscription.rb index 75ea72c08..612806c5f 100644 --- a/lib/cancel_billing_subscription.rb +++ b/app/legacy_lib/cancel_billing_subscription.rb @@ -11,6 +11,7 @@ def self.with_stripe(nonprofit) rescue ParamValidation::ValidationError => e 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 @@ -30,6 +31,7 @@ def self.with_stripe(nonprofit) status: 'active' }) + BillingSubscription::clear_cache(nonprofit) return {json:{}, status: :ok} end end diff --git a/lib/errors/cc_org_error.rb b/app/legacy_lib/cc_org_error.rb similarity index 100% rename from lib/errors/cc_org_error.rb rename to app/legacy_lib/cc_org_error.rb diff --git a/lib/errors/charge_error.rb b/app/legacy_lib/charge_error.rb similarity index 100% rename from lib/errors/charge_error.rb rename to app/legacy_lib/charge_error.rb diff --git a/app/legacy_lib/chunked_uploader/s3.rb b/app/legacy_lib/chunked_uploader/s3.rb new file mode 100644 index 000000000..53100e7f9 --- /dev/null +++ b/app/legacy_lib/chunked_uploader/s3.rb @@ -0,0 +1,27 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module ChunkedUploader + class S3 + + S3_BUCKET_NAME = Settings.aws.bucket_name + + # Upload a string to s3 using chunks instead of all as one string. This is useful reducing memory usage on huge files + # @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={}) + s3 = ::Aws::S3::Resource.new + bucket = s3.bucket(S3_BUCKET_NAME) + object = bucket.object(path) + content_type = metadata[:content_type] ? metadata[:content_type] : nil + content_disposition = metadata[:content_disposition] ? metadata[:content_disposition] : nil + + object.upload_stream(temp_file:true, acl: 'public-read', content_type: content_type, content_disposition: content_disposition) do |write_stream| + chunk_enum.each do |chunk| + write_stream << chunk + end + end + + object.public_url.to_s + end + end +end \ No newline at end of file diff --git a/lib/construct/construct_nonprofit.rb b/app/legacy_lib/construct_nonprofit.rb similarity index 80% rename from lib/construct/construct_nonprofit.rb rename to app/legacy_lib/construct_nonprofit.rb index 7fa5d263b..c482adba1 100644 --- a/lib/construct/construct_nonprofit.rb +++ b/app/legacy_lib/construct_nonprofit.rb @@ -1,10 +1,8 @@ # 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? diff --git a/lib/controllers/campaign_helper.rb b/app/legacy_lib/controllers/campaign_helper.rb similarity index 100% rename from lib/controllers/campaign_helper.rb rename to app/legacy_lib/controllers/campaign_helper.rb diff --git a/lib/controllers/event_helper.rb b/app/legacy_lib/controllers/event_helper.rb similarity index 100% rename from lib/controllers/event_helper.rb rename to app/legacy_lib/controllers/event_helper.rb diff --git a/lib/controllers/nonprofit_helper.rb b/app/legacy_lib/controllers/nonprofit_helper.rb similarity index 87% rename from lib/controllers/nonprofit_helper.rb rename to app/legacy_lib/controllers/nonprofit_helper.rb index a62cf0785..7aae64012 100644 --- a/lib/controllers/nonprofit_helper.rb +++ b/app/legacy_lib/controllers/nonprofit_helper.rb @@ -15,12 +15,6 @@ def authenticate_nonprofit_admin! 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? return false if params[:preview] return false unless current_nonprofit_without_exception @@ -56,4 +50,14 @@ def donation_stub ) end + def reject_for_deactivated_nonprofits + if current_nonprofit&.nonprofit_deactivation&.deactivated + render plain: '', status: :unauthorized + end + end + + def must_block?(nonprofit=nil) + (nonprofit || current_nonprofit)&.miscellaneous_np_info&.temp_block + end + end diff --git a/lib/copy_naming_algorithm.rb b/app/legacy_lib/copy_naming_algorithm.rb similarity index 100% rename from lib/copy_naming_algorithm.rb rename to app/legacy_lib/copy_naming_algorithm.rb diff --git a/lib/create/create_campaign.rb b/app/legacy_lib/create_campaign.rb similarity index 100% rename from lib/create/create_campaign.rb rename to app/legacy_lib/create_campaign.rb diff --git a/lib/create/create_campaign_gift.rb b/app/legacy_lib/create_campaign_gift.rb similarity index 81% rename from lib/create/create_campaign_gift.rb rename to app/legacy_lib/create_campaign_gift.rb index 17c15bbf3..b901f88bc 100644 --- a/lib/create/create_campaign_gift.rb +++ b/app/legacy_lib/create_campaign_gift.rb @@ -34,15 +34,17 @@ def self.create(params) 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 + billing_plan = donation.nonprofit.billing_plan + 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) + unless donation.recurring_donation.amount >= campaign_gift_option.amount_recurring # || (donation.recurring_donation.amount - CalculateFees.for_single_amount(donation.recurring_donation.amount, billing_plan.percentage_fee) == campaign_gift_option.amount_recurring) + AdminMailer.delay.notify_failed_gift(donation, donation.payments.first, 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) + unless donation.amount >= (campaign_gift_option.amount_one_time) # || (donation.amount - CalculateFees.for_single_amount(donation.amount, billing_plan.percentage_fee) == campaign_gift_option.amount_one_time) + AdminMailer.delay.notify_failed_gift(donation,donation.payments.first, 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 @@ -55,7 +57,7 @@ def self.create(params) return gift end end - AdminMailer.delay.notify_failed_gift(donation, campaign_gift_option) + AdminMailer.delay.notify_failed_gift(donation,donation.payments.first, campaign_gift_option) raise ParamValidation::ValidationError.new("#{params[:campaign_gift_option_id]} has no more inventory", {:key => :campaign_gift_option_id}) end diff --git a/lib/create/create_campaign_gift_option.rb b/app/legacy_lib/create_campaign_gift_option.rb similarity index 100% rename from lib/create/create_campaign_gift_option.rb rename to app/legacy_lib/create_campaign_gift_option.rb diff --git a/lib/create/create_custom_field_join.rb b/app/legacy_lib/create_custom_field_join.rb similarity index 100% rename from lib/create/create_custom_field_join.rb rename to app/legacy_lib/create_custom_field_join.rb diff --git a/lib/create/create_custom_field_master.rb b/app/legacy_lib/create_custom_field_master.rb similarity index 100% rename from lib/create/create_custom_field_master.rb rename to app/legacy_lib/create_custom_field_master.rb diff --git a/lib/create/create_peer_to_peer_campaign.rb b/app/legacy_lib/create_peer_to_peer_campaign.rb similarity index 80% rename from lib/create/create_peer_to_peer_campaign.rb rename to app/legacy_lib/create_peer_to_peer_campaign.rb index 854ca4dbb..898fffceb 100644 --- a/lib/create/create_peer_to_peer_campaign.rb +++ b/app/legacy_lib/create_peer_to_peer_campaign.rb @@ -8,7 +8,7 @@ def self.create(campaign_params, profile_id) 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, :widget_description_id) p2p_params.merge!(parent_campaign.child_params) profile = Profile.find(profile_id) @@ -16,15 +16,18 @@ def self.create(campaign_params, profile_id) algo = SlugP2pCampaignNamingAlgorithm.new(p2p_params[:nonprofit_id]) p2p_params[:slug] = algo.create_copy_name(base_slug) + #child campaigns are always in dollars, not supporters + p2p_params[:goal_is_in_supporters] = false + campaign = Campaign.create(p2p_params) campaign.published = true 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 + 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 return { errors: campaign.errors.messages }.as_json unless campaign.errors.empty? diff --git a/lib/create/stripe/create_stripe_account.rb b/app/legacy_lib/create_stripe_account.rb similarity index 100% rename from lib/create/stripe/create_stripe_account.rb rename to app/legacy_lib/create_stripe_account.rb diff --git a/lib/create/create_tag_master.rb b/app/legacy_lib/create_tag_master.rb similarity index 100% rename from lib/create/create_tag_master.rb rename to app/legacy_lib/create_tag_master.rb diff --git a/lib/cypher.rb b/app/legacy_lib/cypher.rb similarity index 100% rename from lib/cypher.rb rename to app/legacy_lib/cypher.rb diff --git a/lib/delayed_job_helper.rb b/app/legacy_lib/delayed_job_helper.rb similarity index 100% rename from lib/delayed_job_helper.rb rename to app/legacy_lib/delayed_job_helper.rb diff --git a/app/legacy_lib/delete_campaign.rb b/app/legacy_lib/delete_campaign.rb new file mode 100644 index 000000000..5de074100 --- /dev/null +++ b/app/legacy_lib/delete_campaign.rb @@ -0,0 +1,15 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module DeleteCampaign + + # This is NOT recommended. We only do this in special cases as campaigns + # should never be totally deleted, only "hidden" + def self.delete(campaign) + if safe_to_delete?(campaign) + campaign.destroy + end + end + + def self.safe_to_delete?(campaign) + campaign.payments.none? && campaign.donations.none? && campaign.activities.none? && (campaign.child_campaign? || campaign.children_campaigns.none?) + end +end \ No newline at end of file diff --git a/lib/delete/delete_campaign_gift_option.rb b/app/legacy_lib/delete_campaign_gift_option.rb similarity index 100% rename from lib/delete/delete_campaign_gift_option.rb rename to app/legacy_lib/delete_campaign_gift_option.rb diff --git a/lib/delete/delete_custom_field_joins.rb b/app/legacy_lib/delete_custom_field_joins.rb similarity index 100% rename from lib/delete/delete_custom_field_joins.rb rename to app/legacy_lib/delete_custom_field_joins.rb diff --git a/lib/email.rb b/app/legacy_lib/email.rb similarity index 80% rename from lib/email.rb rename to app/legacy_lib/email.rb index 4b34cb5df..d58188566 100644 --- a/lib/email.rb +++ b/app/legacy_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[^ ]+@[^ ]+\.[^ ]+\z/i #PsqlRegex ||= '^[A-Za-z0-9._%-]+@[A-Za-z0-9.-]+[.][A-Za-z]+$' end diff --git a/lib/errors/expired_token_error.rb b/app/legacy_lib/expired_token_error.rb similarity index 100% rename from lib/errors/expired_token_error.rb rename to app/legacy_lib/expired_token_error.rb diff --git a/app/legacy_lib/export_payments.rb b/app/legacy_lib/export_payments.rb new file mode 100644 index 000000000..340580ca5 --- /dev/null +++ b/app/legacy_lib/export_payments.rb @@ -0,0 +1,273 @@ +# 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 }, + 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: 'ExportPayments', parameters: params.to_json) + + DelayedJobHelper.enqueue_job(ExportPayments, :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/payments-#{export.id}-#{file_date}.csv" + + url = CHUNKED_UPLOADER.upload(filename, for_export_enumerable(npo_id, params, 15000).map{|i| i.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 + if export + export.status = :failed + export.exception = e.to_s + export.ended = Time.now + export.save! + + begin + user ||= User.where('id = ?', user_id).first + if user + ExportMailer.delay.export_payments_failed_notification(export) + end + rescue + end + raise e + end + raise e + end + + private + + def self.for_export_enumerable(npo_id, query, chunk_limit=15000) + ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, + query: {required:true, is_hash: true}}) + + 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) + QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) do + expr = QueryPayments.full_search_expr(npo_id, query) + .select(*export_selects(query[:export_format_id])) + .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, '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') + .left_outer_join('misc_payment_infos', 'payments.id = misc_payment_infos.payment_id') + end + end + + def self.export_selects(export_format_id) + if(export_format_id.present?) + return build_export_select_using_export_format(ExportFormat.find(export_format_id)) + end + ["to_char(payments.date::timestamptz at time zone COALESCE(nonprofits.timezone, \'UTC\'), 'YYYY-MM-DD HH24:MI:SS TZ') 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'] + .concat(QuerySupporters.supporter_export_selections(:anonymous)) + .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.anonymous OR supporters.anonymous) AS "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', + "CASE WHEN payments.kind = 'RecurringDonation' + THEN to_char(donations.created_at::timestamptz at time zone COALESCE(nonprofits.timezone, \'UTC\'), 'YYYY-MM-DD HH24:MI:SS TZ') + ELSE '' END AS \"Recurring Donation Started At\"", + 'coalesce(nullif(misc_payment_infos.fee_covered, false), false) AS "Fee Covered by Supporter"' + ]) + end + + def self.build_export_select_using_export_format(export_format) + build_payments_select(export_format) + .concat(QuerySupporters.supporter_export_selections(:anonymous)) + .concat(build_donations_and_campaigns_select(export_format)) + end + + def self.build_payments_select(export_format) + custom_names = build_custom_names_for_payments(export_format.custom_columns_and_values) + date_format = export_format.date_format || 'YYYY-MM-DD HH24:MI:SS TZ' + payments_select = ["to_char(payments.date::timestamptz at time zone COALESCE(nonprofits.timezone, \'UTC\'), '#{date_format}') AS #{custom_names['payments.date']}"] + + if(export_format.show_currency) + payments_select.concat([ + "#{money_with_currency('payments.gross_amount')} AS #{custom_names['payments.gross_amount']}", + "#{money_with_currency('payments.fee_total')} AS #{custom_names['payments.fee_total']}", + "#{money_with_currency('payments.net_amount')} AS #{custom_names['payments.net_amount']}" + ]) + else + payments_select.concat([ + "#{money_without_currency('payments.gross_amount')} AS #{custom_names['payments.gross_amount']}", + "#{money_without_currency('payments.fee_total')} AS #{custom_names['payments.fee_total']}", + "#{money_without_currency('payments.net_amount')} AS #{custom_names['payments.net_amount']}" + ]) + end + payments_select.concat([build_custom_value_clause('payments.kind', export_format.custom_columns_and_values, custom_names['payments.kind'])]) + end + + def self.build_donations_and_campaigns_select(export_format) + custom_columns_and_values = export_format.custom_columns_and_values + custom_names = build_custom_names_for_donations_and_campaigns(custom_columns_and_values) + date_format = export_format.date_format || 'YYYY-MM-DD HH24:MI:SS TZ' + [ + build_custom_value_clause('donations.designation', custom_columns_and_values, custom_names['donations.designation'], "coalesce(donations.designation, 'None')"), + "#{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\"", + build_custom_value_clause('donations.anonymous OR supporters.anonymous', custom_columns_and_values, custom_names['donations.anonymous OR supporters.anonymous'], '(donations.anonymous OR supporters.anonymous)'), + build_custom_value_clause('donations.comment', custom_columns_and_values, '"Comment"'), + build_custom_value_clause('campaigns_for_export.name', custom_columns_and_values, custom_names['campaigns_for_export.name'], "coalesce(nullif(campaigns_for_export.name, ''), 'None')"), + "campaigns_for_export.id AS #{custom_names['campaigns_for_export.id']}", + "coalesce(nullif(campaigns_for_export.creator_email, ''), '') AS #{custom_names['campaigns_for_export.creator_email']}", + build_custom_value_clause('campaign_gift_options.name', custom_columns_and_values, custom_names['campaign_gift_options.name'], "coalesce(nullif(campaign_gift_options.name, ''), 'None')"), + build_custom_value_clause('campaigns_for_export.name', custom_columns_and_values, custom_names['campaigns_for_export.name'], "coalesce(nullif(campaign_gift_options.name, ''), 'None')"), + build_custom_value_clause('events_for_export.name', custom_columns_and_values, custom_names['events_for_export.name']), + "payments.id AS #{custom_names['payments.id']}", + "offsite_payments.check_number AS #{custom_names['offsite_payments.check_number']}", + build_custom_value_clause('donations.comment', custom_columns_and_values, custom_names['donations.comment']), + "CASE WHEN payments.kind = 'RecurringDonation' + THEN to_char(donations.created_at::timestamptz at time zone COALESCE(nonprofits.timezone, \'UTC\'), '#{date_format}') + ELSE '' END AS #{custom_names['donations.created_at']}", + build_custom_value_clause('misc_payment_infos.fee_covered', custom_columns_and_values, custom_names['misc_payment_infos.fee_covered'], "coalesce(nullif(misc_payment_infos.fee_covered, false), false)") + ] + end + + def self.build_custom_value_clause(column_name, custom_columns_and_values, custom_column_name, column_treatment = column_name) + custom_values = custom_columns_and_values&.dig(column_name)&.dig('custom_values') + if(custom_values.present?) + return build_custom_values_switch_case(custom_values, column_treatment, custom_column_name) + end + "#{column_treatment} AS #{custom_column_name}" + end + + def self.build_custom_values_switch_case(custom_values, column_name, custom_column_name) + statement = "CASE " + custom_values.each do |original_value, new_value| + statement << "WHEN #{column_name} = '#{original_value}' THEN '#{new_value}' " + end + statement << "ELSE #{column_name} END AS #{custom_column_name}" + end + + def self.build_custom_names_for_payments(custom_names) + { + 'payments.date' => custom_names&.dig('payments.date')&.dig('custom_name') || 'date', + 'payments.gross_amount' => custom_names&.dig('payments.gross_amount')&.dig('custom_name') || 'gross_amount', + 'payments.fee_total' => custom_names&.dig('payments.fee_total')&.dig('custom_name') || 'fee_total', + 'payments.net_amount' => custom_names&.dig('payments.net_amount')&.dig('custom_name') || 'net_amount', + 'payments.kind' => custom_names&.dig('payments.kind')&.dig('custom_name') || 'type' + } + end + + def self.build_custom_names_for_donations_and_campaigns(custom_names) + { + 'donations.designation' => custom_names&.dig('donations.designation')&.dig('custom_name') || 'designation', + 'donations.anonymous OR supporters.anonymous' => ( + custom_names&.dig('donations.anonymous OR supporters.anonymous') || + custom_names&.dig('donations.anonymous') || + custom_names&.dig('supporters.anonymous') + )&.dig('custom_name') || '"Anonymous?"', + 'campaigns_for_export.name' => custom_names&.dig('campaigns_for_export.name')&.dig('custom_name') || 'campaign', + 'campaigns_for_export.id' => custom_names&.dig('campaigns_for_export.id')&.dig('custom_name') || '"Campaign Id"', + 'campaigns_for_export.creator_email' => custom_names&.dig('campaigns_for_export.creator_email')&.dig('custom_name') || 'campaign_creator_email', + 'campaign_gift_options.name' => custom_names&.dig('campaign_gift_options.name')&.dig('custom_name') || 'campaign_gift_level', + 'events_for_export.name' => custom_names&.dig('events_for_export.name')&.dig('custom_name') || 'event_name', + 'payments.id' => custom_names&.dig('payments.id')&.dig('custom_name') || 'payment_id', + 'offsite_payments.check_number' => custom_names&.dig('offsite_payments.check_number')&.dig('custom_name') || 'check_number', + 'donations.comment' => custom_names&.dig('donations.comment')&.dig('custom_name') || 'donation_note', + 'donations.created_at' => custom_names&.dig('donations.created_at')&.dig('custom_name') || '"Recurring Donation Started At"', + 'misc_payment_infos.fee_covered' => custom_names&.dig('misc_payment_infos.fee_covered')&.dig('custom_name') || '"Fee Covered by Supporter"' + } + end + + def self.money_with_currency(column) + "(#{column} / 100.0)::money::text" + end + + def self.money_without_currency(column) + "ROUND((#{column} / 100.0), 2)::text" + 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') + end + + def self.tickets + Qexpr + .new + .select("payment_id", "MAX(event_id) AS event_id") + .from("tickets") + .group_by("payment_id") + .as("tickets") + end +end diff --git a/app/legacy_lib/export_recurring_donations.rb b/app/legacy_lib/export_recurring_donations.rb new file mode 100644 index 000000000..e33a5150d --- /dev/null +++ b/app/legacy_lib/export_recurring_donations.rb @@ -0,0 +1,144 @@ +# 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_ids, export_type = :requested_by_user_through_ui) + ParamValidation.new({ npo_id: npo_id, params: params, user_ids: user_ids }, + npo_id: { required: true, is_integer: true }, + params: { required: true, is_hash: true }, + user_ids: { required: true, is_array: 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_ids.each do |user_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: 'ExportRecurringDonations', parameters: params.to_json) + DelayedJobHelper.enqueue_job(ExportRecurringDonations, :run_export, [npo_id, params.to_json, user_id, e.id, export_type]) + end + end + + def self.run_export(npo_id, params, user_id, export_id, export_type = :requested_by_user_through_ui) + # 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/recurring_donations-#{export.id}-#{file_date}.csv" + + url = CHUNKED_UPLOADER.upload(filename, QueryRecurringDonations.for_export_enumerable(npo_id, params, 15000).map{|i| i.to_csv}, :content_type => 'text/csv', content_disposition: 'attachment') + export.url = url + export.status = :completed + export.ended = Time.now + export.save! + + notify_about_export_completion(export, export_type) + rescue => e + if export + export.status = :failed + export.exception = e.to_s + export.ended = Time.now + export.save! + if user + notify_about_export_failure(export, export_type) + end + raise e + end + raise e + end + + + def self.run_export_for_active_recurring_donations_to_csv(nonprofit_s3_key, filename, export) + if filename.blank? + file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') + filename = "tmp/json-exports/recurring_donations-#{export.id}-#{file_date}.csv" + end + + bucket = get_bucket(nonprofit_s3_key) + object = bucket.object(filename) + object.upload_stream(temp_file:true, acl: 'private', content_type: 'text/csv', content_disposition: 'attachment') do |write_stream| + write_stream << QueryRecurringDonations.get_active_recurring_for_an_org(export.nonprofit) + end + + object.public_url.to_s + + end + + def self.run_export_for_started_recurring_donations_to_csv(nonprofit_s3_key, filename, export) + if filename.blank? + file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') + filename = "tmp/json-exports/recurring_donations-#{export.id}-#{file_date}.csv" + end + + + bucket = get_bucket(nonprofit_s3_key) + object = bucket.object(filename) + object.upload_stream(temp_file:true, acl: 'private', content_type: 'text/csv', content_disposition: 'attachment') do |write_stream| + write_stream << QueryRecurringDonations.get_new_recurring_for_an_org_during_a_period(export.nonprofit) + end + + object.public_url.to_s + + end + + + def self.get_bucket(nonprofit_s3_key) + if nonprofit_s3_key.present? + nonprofit_s3_key.s3_bucket + else + s3 = ::Aws::S3::Resource.new + bucket = s3.bucket(ChunkedUploader::S3::S3_BUCKET_NAME) + end + end + + private + + def self.notify_about_export_completion(export, export_type) + case export_type + when :failed_recurring_donations_automatic_report + ExportMailer.delay.export_failed_recurring_donations_monthly_completed_notification(export) + when :cancelled_recurring_donations_automatic_report + ExportMailer.delay.export_cancelled_recurring_donations_monthly_completed_notification(export) + else + ExportMailer.delay.export_recurring_donations_completed_notification(export) + end + end + + def self.notify_about_export_failure(export, export_type) + case export_type + when :failed_recurring_donations_automatic_report + ExportMailer.delay.export_failed_recurring_donations_monthly_failed_notification(export) + when :cancelled_recurring_donations_automatic_report + ExportMailer.delay.export_cancelled_recurring_donations_monthly_failed_notification(export) + else + ExportMailer.delay.export_recurring_donations_failed_notification(export) + end + end +end diff --git a/lib/export/export_supporter_notes.rb b/app/legacy_lib/export_supporter_notes.rb similarity index 91% rename from lib/export/export_supporter_notes.rb rename to app/legacy_lib/export_supporter_notes.rb index 83b9316e5..ea3486f23 100644 --- a/lib/export/export_supporter_notes.rb +++ b/app/legacy_lib/export_supporter_notes.rb @@ -49,15 +49,15 @@ def self.run_export(npo_id, params, user_id, export_id) end file_date = Time.now.getutc().strftime('%m-%d-%Y--%H-%M-%S') - filename = "tmp/csv-exports/supporters-notes-#{file_date}.csv" + filename = "tmp/csv-exports/supporters-notes-#{export.id}-#{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') + url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.supporter_note_export_enumerable(npo_id, params, 15000).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) + JobQueue.queue(JobTypes::ExportSupporterNotesCompletedJob, export) rescue => e if export export.status = :failed @@ -65,7 +65,7 @@ def self.run_export(npo_id, params, user_id, export_id) export.ended = Time.now export.save! if user - EmailJobQueue.queue(JobTypes::ExportSupporterNotesFailedJob, export) + JobQueue.queue(JobTypes::ExportSupporterNotesFailedJob, export) end raise e end diff --git a/lib/export/export_supporters.rb b/app/legacy_lib/export_supporters.rb similarity index 90% rename from lib/export/export_supporters.rb rename to app/legacy_lib/export_supporters.rb index c657ffb00..a13683b11 100644 --- a/lib/export/export_supporters.rb +++ b/app/legacy_lib/export_supporters.rb @@ -48,15 +48,14 @@ def self.run_export(npo_id, params, user_id, export_id) end 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') + filename = "tmp/csv-exports/supporters-#{export.id}-#{file_date}.csv" + url = CHUNKED_UPLOADER.upload(filename, QuerySupporters.for_export_enumerable(npo_id, params, 15000).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::ExportSupportersCompletedJob, export) + ExportMailer.delay.export_supporters_completed_notification(export) rescue => e if export export.status = :failed @@ -64,7 +63,7 @@ def self.run_export(npo_id, params, user_id, export_id) export.ended = Time.now export.save! if user - EmailJobQueue.queue(JobTypes::ExportSupportersFailedJob, export) + ExportMailer.delay.export_supporters_failed_notification(export) end raise e end diff --git a/lib/fetch/fetch_background_image.rb b/app/legacy_lib/fetch_background_image.rb similarity index 100% rename from lib/fetch/fetch_background_image.rb rename to app/legacy_lib/fetch_background_image.rb diff --git a/lib/fetch/fetch_campaign.rb b/app/legacy_lib/fetch_campaign.rb similarity index 100% rename from lib/fetch/fetch_campaign.rb rename to app/legacy_lib/fetch_campaign.rb diff --git a/lib/fetch/fetch_event.rb b/app/legacy_lib/fetch_event.rb similarity index 100% rename from lib/fetch/fetch_event.rb rename to app/legacy_lib/fetch_event.rb diff --git a/lib/fetch/fetch_miscellaneous_np_info.rb b/app/legacy_lib/fetch_miscellaneous_np_info.rb similarity index 100% rename from lib/fetch/fetch_miscellaneous_np_info.rb rename to app/legacy_lib/fetch_miscellaneous_np_info.rb diff --git a/app/legacy_lib/fetch_nonprofit.rb b/app/legacy_lib/fetch_nonprofit.rb new file mode 100644 index 000000000..a1f8e9c6f --- /dev/null +++ b/app/legacy_lib/fetch_nonprofit.rb @@ -0,0 +1,15 @@ +# 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.find_via_cached_key_for_location(params[:state_code], params[:city], params[:name]) + elsif params[:nonprofit_id] || params[:id] + return Nonprofit.find_via_cached_id(params[:nonprofit_id] || params[:id]) + elsif administered_nonprofit + administered_nonprofit + end + end + +end + diff --git a/lib/fetch/fetch_nonprofit_email.rb b/app/legacy_lib/fetch_nonprofit_email.rb similarity index 100% rename from lib/fetch/fetch_nonprofit_email.rb rename to app/legacy_lib/fetch_nonprofit_email.rb diff --git a/lib/fetch/stripe/fetch_stripe_account.rb b/app/legacy_lib/fetch_stripe_account.rb similarity index 100% rename from lib/fetch/stripe/fetch_stripe_account.rb rename to app/legacy_lib/fetch_stripe_account.rb diff --git a/lib/fetch/fetch_todo_status.rb b/app/legacy_lib/fetch_todo_status.rb similarity index 83% rename from lib/fetch/fetch_todo_status.rb rename to app/legacy_lib/fetch_todo_status.rb index ac6011985..1bd09463d 100644 --- a/lib/fetch/fetch_todo_status.rb +++ b/app/legacy_lib/fetch_todo_status.rb @@ -7,7 +7,7 @@ def self.for_profile(np) has_background: np.background_image?, has_summary: np.summary?, has_image: np.main_image?, - has_highlight: !np.achievements.join.blank?, + has_highlight: np.has_achievements?, has_services: np.full_description? } end @@ -21,7 +21,7 @@ def self.for_dashboard(np) 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?, + is_verified: np.stripe_account&.verification_status == :verified && np.bank_account.present?, has_thank_you: np.thank_you_note.present? } end diff --git a/lib/format/format/address.rb b/app/legacy_lib/format/address.rb similarity index 100% rename from lib/format/format/address.rb rename to app/legacy_lib/format/address.rb diff --git a/app/legacy_lib/format/csv.rb b/app/legacy_lib/format/csv.rb new file mode 100644 index 000000000..75db7c9de --- /dev/null +++ b/app/legacy_lib/format/csv.rb @@ -0,0 +1,34 @@ +# 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] + # @option opts [TrueClass,FalseClass] titleize_header (true) Whether to titleize headers, i.e. whether to turn "supporter_email" into "Supporter Email" + def self.from_data(arr, titleize_header: true) + return CSV.generate do |csv| + csv << arr.first.keys.map{|k| titleize_header ? k.to_s.titleize : k.to_s} + 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} + 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||[])} + end + end + + end +end diff --git a/lib/format/format/currency.rb b/app/legacy_lib/format/currency.rb similarity index 100% rename from lib/format/format/currency.rb rename to app/legacy_lib/format/currency.rb diff --git a/lib/format/format/date.rb b/app/legacy_lib/format/date.rb similarity index 94% rename from lib/format/format/date.rb rename to app/legacy_lib/format/date.rb index 7d803e3c4..e1a2a2e40 100644 --- a/lib/format/format/date.rb +++ b/app/legacy_lib/format/date.rb @@ -3,16 +3,10 @@ module Format; module Date - 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 diff --git a/lib/format/format/dedication.rb b/app/legacy_lib/format/dedication.rb similarity index 100% rename from lib/format/format/dedication.rb rename to app/legacy_lib/format/dedication.rb diff --git a/lib/format/format/geography.rb b/app/legacy_lib/format/geography.rb similarity index 100% rename from lib/format/format/geography.rb rename to app/legacy_lib/format/geography.rb diff --git a/lib/format/format/html.rb b/app/legacy_lib/format/html.rb similarity index 100% rename from lib/format/format/html.rb rename to app/legacy_lib/format/html.rb diff --git a/lib/format/format/indefinitize.rb b/app/legacy_lib/format/indefinitize.rb similarity index 100% rename from lib/format/format/indefinitize.rb rename to app/legacy_lib/format/indefinitize.rb diff --git a/lib/format/format/interpolate.rb b/app/legacy_lib/format/interpolate.rb similarity index 100% rename from lib/format/format/interpolate.rb rename to app/legacy_lib/format/interpolate.rb diff --git a/lib/format/format/name.rb b/app/legacy_lib/format/name.rb similarity index 100% rename from lib/format/format/name.rb rename to app/legacy_lib/format/name.rb diff --git a/lib/format/format/phone.rb b/app/legacy_lib/format/phone.rb similarity index 100% rename from lib/format/format/phone.rb rename to app/legacy_lib/format/phone.rb diff --git a/lib/format/format/remove_diacritics.rb b/app/legacy_lib/format/remove_diacritics.rb similarity index 100% rename from lib/format/format/remove_diacritics.rb rename to app/legacy_lib/format/remove_diacritics.rb diff --git a/lib/format/format/timezone.rb b/app/legacy_lib/format/timezone.rb similarity index 100% rename from lib/format/format/timezone.rb rename to app/legacy_lib/format/timezone.rb diff --git a/lib/format/format/url.rb b/app/legacy_lib/format/url.rb similarity index 100% rename from lib/format/format/url.rb rename to app/legacy_lib/format/url.rb diff --git a/lib/geocode_model.rb b/app/legacy_lib/geocode_model.rb similarity index 100% rename from lib/geocode_model.rb rename to app/legacy_lib/geocode_model.rb diff --git a/lib/get_data.rb b/app/legacy_lib/get_data.rb similarity index 100% rename from lib/get_data.rb rename to app/legacy_lib/get_data.rb diff --git a/app/legacy_lib/hash.rb b/app/legacy_lib/hash.rb new file mode 100644 index 000000000..80b806864 --- /dev/null +++ b/app/legacy_lib/hash.rb @@ -0,0 +1,7 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class Hash + # Prefills a new Hash from the values in other_hash. If a value in other_hash is nil, then it isn't copied over. + def self.with_defaults_unless_nil(other_hash) + try_convert(other_hash.compact) + end +end diff --git a/lib/health_report.rb b/app/legacy_lib/health_report.rb similarity index 84% rename from lib/health_report.rb rename to app/legacy_lib/health_report.rb index 53e4130c3..2971e77cf 100644 --- a/lib/health_report.rb +++ b/app/legacy_lib/health_report.rb @@ -22,11 +22,9 @@ def self.query_data .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 = Nonprofit.includes(:stripe_account) + .where('created_at > ?', 3.months.ago).select{|i| i.stripe_account.verification_status != :verified} + .map{|np| {id: np.id, name: np.name, stripe_account_id: np.stripe_account_id}} return { charges_count: charges['count'], @@ -40,7 +38,7 @@ def self.query_data # Given a hash of data, formats it into a multi-line string def self.format_data data - disabled_nps = Format::Csv.from_array(data[:recently_disabled_nps]) + disabled_nps = Format::Csv.from_data(data[:recently_disabled_nps]) return %Q( Transaction Metrics for the last 24hrs: diff --git a/app/legacy_lib/httparty/logger.rb b/app/legacy_lib/httparty/logger.rb new file mode 100644 index 000000000..20616a887 --- /dev/null +++ b/app/legacy_lib/httparty/logger.rb @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require_relative './logger/commitchange_logger' +require_relative './logger/mailchimp_logger' +require_relative './logger/full_contact_logger' diff --git a/app/legacy_lib/httparty/logger/commitchange_logger.rb b/app/legacy_lib/httparty/logger/commitchange_logger.rb new file mode 100644 index 000000000..3070548dd --- /dev/null +++ b/app/legacy_lib/httparty/logger/commitchange_logger.rb @@ -0,0 +1,130 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# based on https://github.com/jnunemaker/httparty/blob/b8f769e9f3133ec1dfa7fb2800ff3542d4248099/lib/httparty/logger/curl_formatter.rb which is +# under the following license: +# Copyright (c) 2008 John Nunemaker + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. + +module HTTParty + module Logger + class CommitchangeLogger + attr_accessor :level, :logger, :output_type + + def initialize(logger, level, output_type) + @logger = logger + @level = level.to_sym + @output_type = output_type + @messages = [] + end + + def format(request, response) + @request = request + @response = response + + log_request + log_response + + logger.public_send level, JSON::generate(@output) + end + + attr_reader :request, :response + + def output_hash + @output ||= { + type: output_type, + request: {}, + response: {} + } + end + + def output_request + output_hash[:request] + end + + def output_response + output_hash[:response] + end + + def output_headers + output_response[:headers] ||= {} + end + + def log_request + log_url + log_headers + log_query + log_request_body + end + + def log_url + http_method = request.http_method.name.split('::').last.upcase + uri = if request.options[:base_uri] + request.options[:base_uri] + request.path.path + else + request.path.to_s + end + + output_request[:url] = uri + end + + def log_headers + return unless request.options[:headers] && request.options[:headers].size > 0 + output_request[:headers] = request.options[:headers] + end + + def log_query + return unless request.options[:query] + + output_request[:query] = request.options[:query] + end + + def log_request_body + output_request[:body] = request.raw_body if request.raw_body + end + + def log_response + log_response_http_version + log_response_code + log_response_headers + log_response_body + end + + def log_response_http_version + output_response[:http_version] = response.http_version + end + + def log_response_code + output_response[:response_code] = response.code + end + + def log_response_headers + headers = response.respond_to?(:headers) ? response.headers : response + response.each_header do |response_header| + output_headers[response_header] = headers[response_header] + end + end + + def log_response_body + output_response[:body] = response.body + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/httparty/logger/full_contact_logger.rb b/app/legacy_lib/httparty/logger/full_contact_logger.rb new file mode 100644 index 000000000..9a1d8aa91 --- /dev/null +++ b/app/legacy_lib/httparty/logger/full_contact_logger.rb @@ -0,0 +1,34 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# based on https://github.com/jnunemaker/httparty/blob/b8f769e9f3133ec1dfa7fb2800ff3542d4248099/lib/httparty/logger/curl_formatter.rb which is +# under the following license: +# Copyright (c) 2008 John Nunemaker + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +require_relative './commitchange_logger' +module HTTParty + module Logger + class FullContactLogger < CommitchangeLogger + def initialize(logger, level) + super(logger, level, 'full_contact') + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/httparty/logger/mailchimp_logger.rb b/app/legacy_lib/httparty/logger/mailchimp_logger.rb new file mode 100644 index 000000000..2e52289e3 --- /dev/null +++ b/app/legacy_lib/httparty/logger/mailchimp_logger.rb @@ -0,0 +1,34 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# based on https://github.com/jnunemaker/httparty/blob/b8f769e9f3133ec1dfa7fb2800ff3542d4248099/lib/httparty/logger/curl_formatter.rb which is +# under the following license: +# Copyright (c) 2008 John Nunemaker + +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: + +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. + +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +require_relative './commitchange_logger' +module HTTParty + module Logger + class MailchimpLogger < ::HTTParty::Logger::CommitchangeLogger + def initialize(logger, level) + super(logger, level, 'mailchimp') + end + end + end +end \ No newline at end of file diff --git a/lib/image.rb b/app/legacy_lib/image.rb similarity index 100% rename from lib/image.rb rename to app/legacy_lib/image.rb diff --git a/lib/import/import_civicrm_payments.rb b/app/legacy_lib/import_civicrm_payments.rb similarity index 100% rename from lib/import/import_civicrm_payments.rb rename to app/legacy_lib/import_civicrm_payments.rb diff --git a/app/legacy_lib/import_onecause_event_donations.rb b/app/legacy_lib/import_onecause_event_donations.rb new file mode 100644 index 000000000..17da1249b --- /dev/null +++ b/app/legacy_lib/import_onecause_event_donations.rb @@ -0,0 +1,136 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module ImportOnecauseEventDonations + # + # Import from a Onecause Event Donation export + # + # @param [Event] event the event the donations happened at + # @param [TicketLevel] event the ticket level used for users who don't have a ticket + # @param [Array] csv csv import file + # + def self.import(event, ticket_level, csv) + Qx.transaction do + np = event.nonprofit + + bidder_groups = csv.group_by {|a| a['Bidder #']} + bidder_groups.keys.each do |i| + payment_row,non_payment = bidder_groups[i].partition{|row| row['Action'] == 'Payment'} + + payment_row = payment_row.select {|i| i['Payment Status'] == 'Approved'}.first + + if payment_row + supporter_info_row = payment_row + else + supporter_info_row = non_payment.first + end + + supporter_name = supporter_info_row['First Name'] + " " + supporter_info_row['Last Name'] + supporter = winnow_to_supporter(event, np, supporter_name, supporter_info_row['Email']) + + + unless supporter + supporter = Supporter.find(InsertSupporter.create_or_update(np.id, { + name: supporter_name, + email: supporter_info_row['Email'], + city: supporter_info_row['City'], + state_code: supporter_info_row['State'], + zip_code: supporter_info_row['Zip'], + phone: supporter_info_row['Phone #'], + organization: supporter_info_row['Company'] + })['id']) + end + + ticket = winnow_tickets(event, supporter) + + unless ticket + # we create new ticket + ticket = InsertTickets.create({ + tickets: [{quantity: 1, ticket_level_id: ticket_level.id}], + event_id: event.id, + nonprofit_id: np.id, + supporter_id: supporter.id + }, true)['tickets'][0] + else + ticket = ticket.attributes + end + + if payment_row + # do offsite donation for the supporter + donation = InsertDonation.offsite({ + 'amount': payment_row['Payment Amount'].to_i * 100, + 'nonprofit_id': np.id, + 'supporter_id': supporter.id, + 'event_id': event.id, + 'offsite_payment': {} + }.with_indifferent_access) + end + + + + notes = create_notes(payment_row, non_payment) + + notes = ticket['note'] ? ticket['note'] + '\n' + notes : notes + # edit the ticket.notes for the supporter + UpdateTickets.update({event_id: event.id, ticket_id: ticket['id'], note: notes}) + end + end + end + + + def self.winnow_to_supporter(event, np, name, email=nil) + if email + possible_supporters = np.supporters.not_deleted.where('email = ? ', email) + else + possible_supporters = np.supporters.not_deleted.where('name = ?', name) + end + + if possible_supporters.none? + return nil + elsif possible_supporters.one? + return possible_supporters.first + end + + tickets_for_supporters = event.tickets.where('supporter_id IN (?)', possible_supporters.map{|i| i.id}) + + if tickets_for_supporters.none? + return possible_supporters.first + elsif tickets_for_supporters.one? + return tickets_for_supporters.first.supporter + else + return Supporter.find(tickets_for_supporters.map{|i| i.supporter_id}.uniq.first) + end + + end + + def self.winnow_tickets(event, supporter) + return event.tickets.where('supporter_id = ?', supporter.id).first + end + + def self.create_notes(p_row, np_rows) + bidder_num = np_rows.first['Bidder #'] + table_num = np_rows.first['Table #'] + user_def_1 = np_rows.first['User Defined 1'] + user_def_2 = np_rows.first['User Defined 2'] + notes = np_rows.first['Notes'] + + output = [] + + output.push("Bidder #: #{bidder_num}") if bidder_num + output.push("Table #: #{table_num}") if table_num + output.push("User Defined 1: #{user_def_1}") if user_def_1 + output.push("User Defined 2: #{user_def_2}") if user_def_2 + + + output.push('Payments:') + + np_rows.each do |row| + row_str = "Item ##{row['Item #']}, #{row['Item Name']} -- $#{row['Amount']}, Value: $#{row['Value'] || 0}, Item/Charge Type: #{row['Item/Charge Type']}" + output.push("- #{row_str}") + end + + + output.push("Notes: #{notes}") if notes + + return output.join("\n") + end + +end \ No newline at end of file diff --git a/lib/include_asset.rb b/app/legacy_lib/include_asset.rb similarity index 100% rename from lib/include_asset.rb rename to app/legacy_lib/include_asset.rb diff --git a/app/legacy_lib/insert_activities.rb b/app/legacy_lib/insert_activities.rb new file mode 100644 index 000000000..a2c08359f --- /dev/null +++ b/app/legacy_lib/insert_activities.rb @@ -0,0 +1,159 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'qx' +require 'active_support/core_ext' +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"] + end + + # These line up with the above columns + def self.defaults + now = Time.current + ["'created' AS action_type", "'f' AS public", "'#{now}'", "'#{now}'"] + end + + def self.for_recurring_donations(payment_ids) + insert_recurring_donations_expr + .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" + ])) + .from(:payments) + .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) + .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" + ])) + .from(:payments) + .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) + .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" + ])) + .from(:tickets) + .left_outer_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) + .execute + end + + def self.insert_refunds_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, '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") + .where("payments.kind='Refund'") + end + + def self.for_supporter_notes(ids) + insert_supporter_notes_expr + .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" + ])) + .from(:supporter_notes) + .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) + .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" + ])) + .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") + end + + +end diff --git a/lib/insert/insert_bank_account.rb b/app/legacy_lib/insert_bank_account.rb similarity index 91% rename from lib/insert/insert_bank_account.rb rename to app/legacy_lib/insert_bank_account.rb index c96f1b705..9056fde39 100644 --- a/lib/insert/insert_bank_account.rb +++ b/app/legacy_lib/insert_bank_account.rb @@ -26,11 +26,7 @@ def self.with_stripe(nonprofit, user, params) } }) - unless (nonprofit.vetted) - raise ArgumentError.new "#{nonprofit.id} is not vetted." - end - - stripe_acct = Stripe::Account.retrieve(StripeAccount.find_or_create(nonprofit.id)) + stripe_acct = Stripe::Account.retrieve(StripeAccountUtils.find_or_create(nonprofit.id)) nonprofit.reload #this shouldn't be possible but we'll check any who if (nonprofit.stripe_account_id.blank?) diff --git a/lib/insert/insert_card.rb b/app/legacy_lib/insert_card.rb similarity index 97% rename from lib/insert/insert_card.rb rename to app/legacy_lib/insert_card.rb index dfb7709ef..7d3c2f42d 100644 --- a/lib/insert/insert_card.rb +++ b/app/legacy_lib/insert_card.rb @@ -1,5 +1,4 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'hash' module InsertCard @@ -37,7 +36,7 @@ def self.with_stripe(card_data, stripe_account_id=nil, event_id=nil, current_use # 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 ) + card_data = card_data.slice(: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 @@ -111,8 +110,10 @@ def self.with_stripe(card_data, stripe_account_id=nil, event_id=nil, current_use 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} rescue e + Airbrake.notify(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} rescue e + Airbrake.notify(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} end diff --git a/lib/insert/insert_charge.rb b/app/legacy_lib/insert_charge.rb similarity index 77% rename from lib/insert/insert_charge.rb rename to app/legacy_lib/insert_charge.rb index 15433c830..cd05b9f0f 100644 --- a/lib/insert/insert_charge.rb +++ b/app/legacy_lib/insert_charge.rb @@ -1,11 +1,11 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'psql' -require 'qexpr' -require 'calculate/calculate_fees' -require 'stripe' -require 'get_data' -require 'active_support/core_ext' -require 'query/billing_plans' +# require 'psql' +# require 'qexpr' +# require 'calculate/calculate_fees' +# require 'stripe' +# require 'get_data' +# require 'active_support/core_ext' + require 'stripe_account' unless !Settings.payment_provider.stripe_connect module InsertCharge @@ -65,6 +65,7 @@ def self.with_stripe(data) unless card.holder == supporter if (data[:old_donation]) #these are not new donations so we let them fly (for now) + Airbrake.notify(ParamValidation::ValidationError.new("#{data[:card_id]} does not belong to this supporter #{supporter.id} as warning", {:key => :card_id})) else raise ParamValidation::ValidationError.new("#{data[:card_id]} does not belong to this supporter #{supporter.id}", {:key => :card_id}) end @@ -74,8 +75,9 @@ def self.with_stripe(data) # 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]) + stripe_account_id = StripeAccountUtils.find_or_create(data[:nonprofit_id]) rescue => e + Airbrake.notify(e, other_data: data) raise e end nonprofit_currency = Qx.select(:currency).from(:nonprofits).where("id=$id", id: data[:nonprofit_id]).execute.first['currency'] @@ -85,25 +87,31 @@ def self.with_stripe(data) amount: data[:amount], currency: nonprofit_currency, description: data[:statement], - statement_descriptor: data[:statement][0..21].gsub(/[<>"']/,''), + statement_descriptor_suffix: 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 + stripe_account_id = StripeAccountUtils.find_or_create(data[:nonprofit_id]) + + # 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}] + stripe_cust = Stripe::Customer.retrieve({id: stripe_customer_id, expand: ['default_source']}, {stripe_version: "2019-09-09"}) + transfer_data = {transfer_data: { destination: stripe_account_id}, on_behalf_of: stripe_account_id} + + # Get the percentage fee on the nonprofit's billing + fee = Nonprofit.find(data[:nonprofit_id]).calculate_fee(amount: data[:amount], source: stripe_cust.default_source) + + stripe_charge_data[:application_fee_amount]= fee + + params = [stripe_charge_data.merge(transfer_data), {stripe_version: "2019-09-09"}] + rescue => e + Airbrake.notify(e,other_data: {reason: 'a payment that should never happen'}) + raise e end else fee=0 @@ -115,8 +123,10 @@ def self.with_stripe(data) stripe_charge = Stripe::Charge.create(*params) rescue Stripe::CardError => e failure_message = "There was an error with your card: #{e.json_body[:error][:message]}" + Airbrake.notify(e) rescue Stripe::StripeError => e failure_message = "We're sorry, but something went wrong. We've been notified about this issue." + Airbrake.notify(e) end @@ -125,9 +135,9 @@ def self.with_stripe(data) charge.amount = data[:amount] charge.fee = fee - charge.stripe_charge_id = GetData.chain(stripe_charge, :id) + charge.stripe_charge_id = stripe_charge&.id charge.failure_message = failure_message - charge.status = GetData.chain(stripe_charge, :paid) ? 'pending' : 'failed' + charge.status = stripe_charge&.paid ? 'pending' : 'failed' charge.card = card charge.donation = Donation.where('id = ?', data[:donation_id]).first charge.supporter = supporter @@ -149,6 +159,11 @@ def self.with_stripe(data) payment.date = data[:date] || result['charge'].created_at payment.save! + + misc = payment.misc_payment_info || payment.create_misc_payment_info + + misc.fee_covered = data[:fee_covered] + misc.save! result['payment'] = payment @@ -159,6 +174,7 @@ def self.with_stripe(data) return result rescue => e + Airbrake.notify(e) raise e end end diff --git a/lib/insert/insert_custom_field_joins.rb b/app/legacy_lib/insert_custom_field_joins.rb similarity index 98% rename from lib/insert/insert_custom_field_joins.rb rename to app/legacy_lib/insert_custom_field_joins.rb index f25a38cd6..651759a24 100644 --- a/lib/insert/insert_custom_field_joins.rb +++ b/app/legacy_lib/insert_custom_field_joins.rb @@ -1,7 +1,5 @@ # 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 diff --git a/lib/insert/insert_direct_debit_detail.rb b/app/legacy_lib/insert_direct_debit_detail.rb similarity index 100% rename from lib/insert/insert_direct_debit_detail.rb rename to app/legacy_lib/insert_direct_debit_detail.rb diff --git a/lib/insert/insert_donation.rb b/app/legacy_lib/insert_donation.rb similarity index 76% rename from lib/insert/insert_donation.rb rename to app/legacy_lib/insert_donation.rb index 86875837f..29f00973f 100644 --- a/lib/insert/insert_donation.rb +++ b/app/legacy_lib/insert_donation.rb @@ -33,6 +33,7 @@ def self.with_stripe(data, current_user=nil) result = {} data[:date] = Time.now + data = amount_from_data(data) data = data.except(:old_donation).except('old_donation') result = result.merge(insert_charge(data)) if result['charge']['status'] == 'failed' @@ -40,11 +41,30 @@ def self.with_stripe(data, current_user=nil) end # Create the donation record result['donation'] = self.insert_donation(data, entities) + trx = entities[:supporter_id].transactions.build(amount: data['amount'], created: data['date']) update_donation_keys(result) + don = trx.donations.build(amount: result['donation'].amount, legacy_donation: result['donation']) + legacy_payment = Payment.find(result['payment']['id']) + stripe_transaction_charge = SubtransactionPayment.new( + legacy_payment: legacy_payment, + paymentable: StripeTransactionCharge.new, + created: legacy_payment.date + ) + stripe_t = trx.build_subtransaction( + subtransactable: StripeTransaction.new(amount: data['amount']), + subtransaction_payments:[ + stripe_transaction_charge + ] + ); + trx.save! + don.save! + stripe_t.save! + stripe_t.subtransaction_payments.each(&:publish_created) + #stripe_t.publish_created + don.publish_created + trx.publish_created result['activity'] = InsertActivities.for_one_time_donations([result['payment'].id]) - 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) + JobQueue.queue(JobTypes::DonationPaymentCreateJob, result['donation'].id, result['payment'].id, entities[:supporter_id].locale) result end @@ -67,10 +87,12 @@ def self.offsite(data) 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) + validate_all_entities(entities) - data = date_from_data(data) + data = amount_from_data(date_from_data(data)) result = {'donation' => self.insert_donation(data.except('offsite_payment'), entities)} + trx = entities[:supporter_id].transactions.build(amount: data['amount'], created: data['date']) + don = trx.donations.build(amount: result['donation'].amount, legacy_donation: result['donation']) result['payment'] = self.insert_payment('OffsitePayment', 0, result['donation']['id'], data) result['offsite_payment'] = Psql.execute( Qexpr.new.insert(:offsite_payments, [ @@ -84,8 +106,30 @@ def self.offsite(data) }) ]).returning('*') ).first + legacy_payment = Payment.find(result['payment']['id']) + offline_transaction_charge_payment = SubtransactionPayment.new( + legacy_payment: legacy_payment, + paymentable: OfflineTransactionCharge.new, + created: legacy_payment.date + ) + off_t = trx.build_subtransaction( + subtransactable: OfflineTransaction.new(amount: data['amount']), + subtransaction_payments:[ + offline_transaction_charge_payment + ], + created: data['date'] + ); + trx.save! + don.save! + off_t.save! + + trx.publish_created + don.publish_created + # off_t.publish_created + offline_transaction_charge_payment.publish_created + result['activity'] = InsertActivities.for_offsite_donations([result['payment']['id']]) - QueueDonations.delay.execute_for_donation(result['donation'].id) + return {status: 200, json: result} end @@ -105,10 +149,8 @@ def self.with_sepa(data) result['donation'] = self.insert_donation(data, entities) update_donation_keys(result) - EmailJobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, result['donation'].id) - EmailJobQueue.queue(JobTypes::DonorDirectDebitNotificationJob, result['donation'].id, locale_for_supporter(result['donation'].supporter.id)) - - QueueDonations.delay.execute_for_donation(result['donation'].id) + JobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, result['donation'].id, result['donation'].payment.id) + JobQueue.queue(JobTypes::DonorDirectDebitNotificationJob, result['donation'].id, result['donation'].supporter.locale) # do this for making test consistent result['activity'] = {} result @@ -117,17 +159,14 @@ def self.with_sepa(data) private def self.get_nonprofit_data(nonprofit_id) - Psql.execute( - Qexpr.new.select(:statement, :name).from(:nonprofits) - .where("id=$id", id: nonprofit_id) - ).first + Nonprofit.find(nonprofit_id) 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" - if payment_provider == :credit_card + if payment_provider == 'credit_card' return InsertCharge.with_stripe({ donation_id: data['donation_id'], kind: kind, @@ -138,9 +177,10 @@ def self.insert_charge(data) nonprofit_id: data['nonprofit_id'], supporter_id: data['supporter_id'], card_id: data['card_id'], - old_donation: data['old_donation'] ? true : false + old_donation: data['old_donation'] ? true : false, + fee_covered: data['fee_covered'] }) - elsif payment_provider == :sepa + elsif payment_provider == 'sepa' return InsertCharge.with_sepa({ donation_id: data['donation_id'], kind: kind, @@ -199,18 +239,15 @@ def self.date_from_data(data) data.merge('date' => data['date'].blank? ? Time.current : Chronic.parse(data['date'])) end - def self.locale_for_supporter(supporter_id) - Psql.execute( - Qexpr.new.select(:locale).from(:supporters) - .where("id=$id", id: supporter_id) - ).first['locale'] + def self.amount_from_data(data) + data.merge('amount' => data['amount'].to_i) end def self.payment_provider(data) if data[:card_id] || data["card_id"] - :credit_card + 'credit_card' elsif data[:direct_debit_detail_id] || data["direct_debit_detail_id"] - :sepa + 'sepa' end end @@ -231,6 +268,13 @@ def self.common_param_validations end def self.validate_entities(entities) + unless entities[:nonprofit_id].can_process_charge? + raise ParamValidation::ValidationError.new("Nonprofit #{entities[:nonprofit_id].id} is not allowed to process charges", key: :nonprofit_id) + end + validate_all_entities(entities) + end + + def self.validate_all_entities(entities) ## is supporter deleted? If supporter is deleted, we error! if entities[:supporter_id].deleted raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} is deleted", key: :supporter_id) diff --git a/lib/insert/insert_duplicate.rb b/app/legacy_lib/insert_duplicate.rb similarity index 82% rename from lib/insert/insert_duplicate.rb rename to app/legacy_lib/insert_duplicate.rb index 60e6e6d33..45590688d 100644 --- a/lib/insert/insert_duplicate.rb +++ b/app/legacy_lib/insert_duplicate.rb @@ -1,6 +1,6 @@ # 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) + def self.campaign(campaign_id, profile_id, new_nonprofit=nil) ParamValidation.new({:campaign_id => campaign_id, :profile_id => profile_id}, { :campaign_id => {:required => true, :is_integer => true}, @@ -25,13 +25,15 @@ def self.campaign(campaign_id, profile_id) dupe.end_datetime = DateTime.now.since(7.days) end + dupe.nonprofit = new_nonprofit if new_nonprofit + dupe.profile = profile dupe.published = false dupe.save! - dupe.update_attribute(:main_image, campaign.main_image) unless !campaign.main_image rescue AWS::S3::Errors::NoSuchKey + dupe.update_attribute(:main_image, campaign.main_image) unless !campaign.main_image rescue Aws::S3::Errors::NoSuchKey - dupe.update_attribute(:background_image, campaign.background_image) unless !campaign.background_image rescue AWS::S3::Errors::NoSuchKey + dupe.update_attribute(:background_image, campaign.background_image) unless !campaign.background_image rescue Aws::S3::Errors::NoSuchKey InsertDuplicate.campaign_gift_options(campaign_id, dupe.id) @@ -39,7 +41,7 @@ def self.campaign(campaign_id, profile_id) end end - def self.event(event_id, profile_id) + def self.event(event_id, profile_id, new_nonprofit=nil) ParamValidation.new({:event_id => event_id, :profile_id => profile_id}, { :event_id => {:required => true, :is_integer => true}, @@ -72,23 +74,35 @@ def self.event(event_id, profile_id) if (we_changed_start_time && dupe.end_datetime) dupe.end_datetime = dupe.start_datetime.since(length_of_event) end - - + + dupe.nonprofit = new_nonprofit if new_nonprofit + dupe.organizer_email = profile.user.email + dupe.profile = profile dupe.published = false dupe.save! - dupe.update_attribute(:main_image, event.main_image) unless !event.main_image rescue AWS::S3::Errors::NoSuchKey + dupe.update_attribute(:main_image, event.main_image) unless !event.main_image rescue Aws::S3::Errors::NoSuchKey - dupe.update_attribute(:background_image, event.background_image) unless !event.background_image rescue AWS::S3::Errors::NoSuchKey + dupe.update_attribute(:background_image, event.background_image) unless !event.background_image rescue Aws::S3::Errors::NoSuchKey InsertDuplicate.ticket_levels(event_id, dupe.id) InsertDuplicate.event_discounts(event_id, dupe.id) + InsertDuplicate.misc_event_info(event, dupe) return dupe end end + def self.misc_event_info(event, dupe) + original_custom_get_tickets_button_label = event&.misc_event_info&.custom_get_tickets_button_label + return unless original_custom_get_tickets_button_label.present? + + dupe.create_misc_event_info + dupe.misc_event_info.custom_get_tickets_button_label = original_custom_get_tickets_button_label + dupe.misc_event_info.save! + end + # 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) diff --git a/app/legacy_lib/insert_email_lists.rb b/app/legacy_lib/insert_email_lists.rb new file mode 100644 index 000000000..c62ff66af --- /dev/null +++ b/app/legacy_lib/insert_email_lists.rb @@ -0,0 +1,44 @@ +# 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 + tags_for_nonprofit = Nonprofit.includes(:tag_masters => :email_list).find(npo_id).tag_masters.not_deleted; + tag_master_ids = tags_for_nonprofit.where('id in (?)', tag_master_ids).pluck(:id) + if tag_master_ids.empty? # no tags were selected; remove all email lists + deleted = tags_for_nonprofit.includes(:email_list).where("email_lists.id IS NOT NULL").references(:email_lists).map{|i| i.email_list} + EmailList.where('id IN (?)', deleted.map{|i| i.id}).delete_all + else # Remove all email lists that exist in the db that are not included in tag_master_ids + deleted = tags_for_nonprofit.includes(:email_list).where("email_lists.tag_master_id NOT IN (?)", tag_master_ids).references(:email_lists).map{|i| i.email_list} + EmailList.where('id IN (?)', deleted.map{|i| i.id}).delete_all + end + mailchimp_lists_to_delete = deleted.map{|i| i.mailchimp_list_id} + result = Mailchimp.delete_mailchimp_lists(npo_id, mailchimp_lists_to_delete) + + return {deleted: deleted.map{|i| {'mailchimp_list_id' => i.mailchimp_list_id}}, deleted_result: result} if tag_master_ids.empty? + + existing = tags_for_nonprofit.includes(:email_list).where('email_lists.tag_master_id IN (?)', tag_master_ids).references(:email_lists) + + tag_master_ids -= existing.map{|i| i.id} + + 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}") + 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 + + Nonprofit.find(npo_id).email_lists.each(&:populate_list_later) + + return {deleted:deleted.map{|i| {'mailchimp_list_id' => i.mailchimp_list_id}}, deleted_result: result, inserted_lists: inserted_lists, inserted_result: lists} + end + +end + diff --git a/app/legacy_lib/insert_full_contact_infos.rb b/app/legacy_lib/insert_full_contact_infos.rb new file mode 100644 index 000000000..5bd28126c --- /dev/null +++ b/app/legacy_lib/insert_full_contact_infos.rb @@ -0,0 +1,183 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'qx' +require 'httparty' +module InsertFullContactInfos + include HTTParty + format :json + logger Rails.logger, :info, :full_contact + + # 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']} + Qx.delete_from('full_contact_jobs').where('TRUE').execute + self.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}}) + .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 = [] + supporter_ids.each do |id| + now = Time.current + result = InsertFullContactInfos.single id + created_ids.push(GetData.hash(result, 'full_contact_info', 'id')) if result.is_a?(Hash) + interval = 0.1 - (Time.current - now) # account for time taken in .single + sleep interval if interval > 0 + end + return 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 + def self.single(supporter_id) + supp = Qx.select('email', 'nonprofit_id').from('supporters').where(id: supporter_id).execute.first + return if supp.nil? || supp['email'].blank? + + begin + response = post("https://api.fullcontact.com/v3/person.enrich", + body: { + "email" => supp['email'], + }.to_json, + headers: { + :authorization => "Bearer #{FULL_CONTACT_KEY}", + "Reporting-Key" => supp['nonprofit_id'].to_s + }) + result = JSON::parse(response.body) + rescue Exception => e + return e + end + + location = result['location'] && result['details']['locations'] && result['details']['locations'][0] + existing = Qx.select('id').from('full_contact_infos').where(supporter_id: supporter_id).ex.first + info_data = { + full_name: result['fullName'], + gender: result['gender'], + city: location && location['city'], + state_code: location && location['regionCode'], + country: location && location['countryCode'], + age_range: result['ageRange'], + location_general: result['location'], + websites: ((result['details'] && result['details']['urls']) || []).map{|h| h['value']}.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 + + if result['details']['photos'].present? + photo_data = result['details']['photos'].map{|h| {type_id: h['label'], url: h['value']}} + 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 + end + + if result['details']['profiles'].present? + profile_data = result['details']['profiles'].map{|k,v| {type_id: v['service'], username: v['username'], uid: v['userid'], bio: v['bio'], url: v['url'], followers: v['followers'], following: v['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 + end + + if result['details'].present? && result['details']['employment'].present? + Qx.delete_from('full_contact_orgs') + .where(full_contact_info_id: full_contact_info['id']) + .execute + org_data = result['details']['employment'].map{|h| + start_date = nil + end_date = nil + start_date = h['start'] && [h['start']['year'], h['start']['month'], h['start']['day']].select{|i| i.present?}.join('-') + end_date = h['end'] && [h['end']['year'], h['end']['month'], h['end']['day']].select{|i| i.present?}.join('-') + { + name: h['name'], + start_date: start_date, + end_date: 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} + + 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 + end + + return { + 'full_contact_info' => full_contact_info, + 'full_contact_photos' => full_contact_photos, + 'full_contact_social_profiles' => full_contact_social_profiles, + 'full_contact_orgs' => full_contact_orgs + } + end + + # 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 + end +end diff --git a/lib/insert/insert_import.rb b/app/legacy_lib/insert_import.rb similarity index 89% rename from lib/insert/insert_import.rb rename to app/legacy_lib/insert_import.rb index 5de386797..67ae61d97 100644 --- a/lib/insert/insert_import.rb +++ b/app/legacy_lib/insert_import.rb @@ -1,12 +1,12 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'qx' -require 'required_keys' -require 'open-uri' -require 'csv' -require 'insert/insert_supporter' -require 'insert/insert_full_contact_infos' -require 'insert/insert_custom_field_joins' -require 'insert/insert_tag_joins' +# require 'qx' +# require 'required_keys' +# require 'open-uri' +# require 'csv' +# require 'insert/insert_supporter' +# require 'insert/insert_full_contact_infos' +# require 'insert/insert_custom_field_joins' +# require 'insert/insert_tag_joins' module InsertImport @@ -40,6 +40,8 @@ def self.from_csv(data) user_email: {required: true} }) + nonprofit = Nonprofit.find(data[:nonprofit_id]) + import = Qx.insert_into(:imports) .values({ date: Time.current, @@ -81,11 +83,10 @@ def self.from_csv(data) # Create supporter record if table_data['supporter'] - table_data['supporter'] = InsertSupporter.defaults(table_data['supporter']) - table_data['supporter']['imported_at'] = Time.current - table_data['supporter']['import_id'] = import['id'] - table_data['supporter']['nonprofit_id'] = data[:nonprofit_id] - table_data['supporter'] = Qx.insert_into(:supporters).values(table_data['supporter']).ts.returning('*').execute.first + table_data['supporter'] = nonprofit.supporters.create( + table_data['supporter'], + imported_at: Time.current, + import: Import.find(import['id'])) supporter_ids.push(table_data['supporter']['id']) imported_count += 1 else @@ -106,7 +107,8 @@ def self.from_csv(data) # Create donation record if table_data['donation'] && table_data['donation']['amount'] # must have amount. donation.date without donation.amount is no good - table_data['donation']['amount'] = (table_data['donation']['amount'].gsub(/[^\d\.]/, '').to_f * 100).to_i + amount_string = table_data['donation']['amount'].gsub(/[^\d\.]/, '') + table_data['donation']['amount'] = (BigDecimal(amount_string.blank? ? 0 : amount_string) * 100).to_i table_data['donation']['supporter_id'] = table_data['supporter']['id'] table_data['donation']['nonprofit_id'] = data[:nonprofit_id] table_data['donation']['date'] = Chronic.parse(table_data['donation']['date']) if table_data['donation']['date'].present? diff --git a/app/legacy_lib/insert_nonprofit_keys.rb b/app/legacy_lib/insert_nonprofit_keys.rb new file mode 100644 index 000000000..98a959c05 --- /dev/null +++ b/app/legacy_lib/insert_nonprofit_keys.rb @@ -0,0 +1,25 @@ +# 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}" + + response = post('https://login.mailchimp.com/oauth2/token', { body: form_data }) + if response['error'] + raise Exception.new(response['error']) + end + + nonprofit_key = Nonprofit.find(npo_id).nonprofit_key + nonprofit_key = Nonprofit.find(npo_id).build_nonprofit_key unless nonprofit_key + + nonprofit_key.mailchimp_token = response['access_token'] + nonprofit_key.save! + + return response['access_token'] + end +end diff --git a/lib/insert/insert_payout.rb b/app/legacy_lib/insert_payout.rb similarity index 80% rename from lib/insert/insert_payout.rb rename to app/legacy_lib/insert_payout.rb index bfc6eabe6..1a8d6b3fe 100644 --- a/lib/insert/insert_payout.rb +++ b/app/legacy_lib/insert_payout.rb @@ -1,13 +1,13 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later # Create a new payout -require 'psql' -require 'qexpr' -require 'query/query_payments' -require 'update/update_charges' -require 'update/update_refunds' -require 'update/update_disputes' -require 'param_validation' +# require 'psql' +# require 'qexpr' +# require 'query/query_payments' +# require 'update/update_charges' +# require 'update/update_refunds' +# require 'update/update_disputes' +# require 'param_validation' module InsertPayout @@ -28,6 +28,14 @@ def self.with_stripe(np_id, data, options={}) }) options ||= {} entities = RetrieveActiveRecordItems.retrieve_from_keys(bigger_data, Nonprofit => :np_id) + if (entities[:np_id].nonprofit_deactivation&.deactivated) + raise ArgumentError.new("Sorry, this account has been deactivated.") + end + + unless (entities[:np_id].can_make_payouts?) + raise ArgumentError.new("Sorry, this account can't make payouts right now.") + end + 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.") @@ -47,6 +55,8 @@ def self.with_stripe(np_id, data, options={}) UpdateRefunds.disburse_all_with_payments(payment_ids) # Mark all disputes as lost_and_paid UpdateDisputes.disburse_all_with_payments(payment_ids) + # mark all balances adjustments as disbursed + UpdateManualBalanceAdjustments.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( @@ -54,7 +64,7 @@ def self.with_stripe(np_id, data, options={}) net_amount: totals['net_amount'], nonprofit_id: np_id, failure_message: stripe_transfer['failure_message'], - status: stripe_transfer.status, + status: stripe_transfer['status'], fee_total: totals['fee_total'], gross_amount: totals['gross_amount'], email: data[:email], @@ -62,11 +72,14 @@ def self.with_stripe(np_id, data, options={}) 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') + bank_name: data[:bank_name], + houid: Payout.generate_houid}]) + .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', 'houid') ).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}})) + # Create ObjectEvent record for payout.created + Payout.find(payout['id'].to_i).publish_created NonprofitMailer.delay.pending_payout_notification(payout['id'].to_i) return payout end @@ -84,8 +97,10 @@ def self.with_stripe(np_id, data, options={}) 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') + bank_name: data[:bank_name], + houid: Payout.generate_houid + }]) + .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', 'houid') ).first return payout end diff --git a/app/legacy_lib/insert_recurring_donation.rb b/app/legacy_lib/insert_recurring_donation.rb new file mode 100644 index 000000000..db35a3039 --- /dev/null +++ b/app/legacy_lib/insert_recurring_donation.rb @@ -0,0 +1,255 @@ +# 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})) + + unless data[:recurring_donation].nil? + + 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} + }) + + 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 = entities.merge(RetrieveActiveRecordItems.retrieve_from_keys(data, {Campaign => :campaign_id, Event => :event_id, Profile => :profile_id}, true)) + + InsertDonation.validate_entities(entities) + + ## 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 + + data['card_id'] = tokenizable.id + + result = {} + data[:date] = Time.now + data = data.merge(payment_provider: payment_provider(data)) + 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 + result = result.merge(InsertDonation.insert_charge(data)) + if result['charge']['status'] == 'failed' + raise ChargeError.new(result['charge']['failure_message']) + end + end + + # Create the donation record + 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) + # Update charge foreign keys + if result['payment'] + InsertDonation.update_donation_keys(result) + # Create the activity record + result['activity'] = InsertActivities.for_recurring_donations([result['payment'].id]) + end + + # Send receipts + JobQueue.queue(JobTypes::DonationPaymentCreateJob, result['donation'].id, result['payment']&.id, entities[:supporter_id].locale) + return result + end + + def self.with_sepa(data) + data = set_defaults(data) + data = data.merge(payment_provider: payment_provider(data)) + result = {} + + if Time.current >= data[:recurring_donation][:start_date] + result = result.merge(InsertDonation.insert_charge(data)) + end + + result['donation'] = Psql.execute(Qexpr.new.insert(:donations, [ + 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 + + if result['payment'] + InsertDonation.update_donation_keys(result) + end + + DonationMailer.delay.nonprofit_payment_notification(result['donation']['id']) + DonationMailer.delay.donor_direct_debit_notification(result['donation']['id'], Supporter.find(result['donation']['supporter_id']).locale) + + { status: 200, json: result } + end + + def self.import_with_stripe(data) + data = data.with_indifferent_access + + ParamValidation.new(data, InsertDonation.common_param_validations + .merge(card_id: {required: true, is_reference:true})) + + unless data[:recurring_donation].nil? + + 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} + }) + + else + data[:recurring_donation] = {} + end + + card = Card.find(data['card_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)) + + InsertDonation.validate_entities(entities) + + ## does the card belong to the supporter? + if card.holder != entities[:supporter_id] + raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not own card #{card.id}", key: :token) + end + + data['card_id'] = card.id + + result = {} + data[:date] = Time.now + data = data.merge(payment_provider: payment_provider(data)) + 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 + puts "we would have charged on #{data}" + + # result = result.merge(InsertDonation.insert_charge(data)) + # if result['charge']['status'] == 'failed' + # raise ChargeError.new(result['charge']['failure_message']) + # end + end + + # Create the donation record + 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) + # Update charge foreign keys + if result['payment'] + InsertDonation.update_donation_keys(result) + # Create the activity record + result['activity'] = InsertActivities.for_recurring_donations([result['payment'].id]) + end + + return 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}) + don.recurring_donation = rd + don.recurring = true + + don.payment.kind = "RecurringDonation" + don.payment.save! + rd.save! + don.save! + + rd + end + + def self.insert_recurring_donation(data, entities) + rd = RecurringDonation.new + rd.amount = data[:amount] + rd.anonymous = data[:anonymous] + rd.nonprofit = entities[:nonprofit_id] + rd.donation = entities[:donation_id] + 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 + + if rd.time_unit == 'month' && rd.interval == 1 && data[:recurring_donation][:paydate].nil? + rd.paydate = convert_date_to_valid_paydate(rd.start_date) + else + rd.paydate = data[:recurring_donation][:paydate] + end + + rd.save! + + + misc = rd.misc_recurring_donation_info || rd.create_misc_recurring_donation_info + misc.fee_covered = data[:fee_covered] + misc.save! + + rd + 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 + + def self.payment_provider(data) + if data[:card_id] + :credit_card + elsif data[:direct_debit_detail_id] + :sepa + end + end + + def self.convert_date_to_valid_paydate(date) + day = date.day + return day > 28 ? 28 : day + end +end diff --git a/app/legacy_lib/insert_refunds.rb b/app/legacy_lib/insert_refunds.rb new file mode 100644 index 000000000..a1a1dcd46 --- /dev/null +++ b/app/legacy_lib/insert_refunds.rb @@ -0,0 +1,154 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module InsertRefunds + + # Refund a given charge, up to its net amount + # params: amount, donation obj + def self.with_stripe(charge, h) + # if Time.now < FEE_SWITCHOVER_TIME + # legacy_refund(charge, h) + # else + modern_refund(charge, h) + # end + end + + + def self.modern_refund(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 = Payment.find(charge['payment_id']) + + 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']})") + end + + refund_data = {'amount' => h['amount'], 'charge' => charge['stripe_charge_id']} + refund_data['reason'] = h['reason'] unless h['reason'].blank? # Stripe will error on blank reason field + + results = InsertRefunds.perform_stripe_refund(nonprofit_id:charge['nonprofit_id'], refund_data:refund_data, charge_date: charge['created_at']) + + Refund.transaction do + + refund = Refund.create!({amount: h['amount'], + comment: h['comment'], + reason: h['reason'], + stripe_refund_id: results[:stripe_refund].id, + charge_id: charge['id'] + }) + + refund.create_misc_refund_info(is_modern: true, stripe_application_fee_refund_id: results[:stripe_app_fee_refund]&.id) + + gross = -(h['amount']) + fees = (results[:stripe_app_fee_refund] && results[:stripe_app_fee_refund].amount) || 0 + net = gross + fees + + # Create a corresponding./run negative payment record + payment = Payment.create!({ + gross_amount: gross, + fee_total: fees, + net_amount: net, + kind: 'Refund', + towards: original_payment.towards, + date: refund.created_at, + nonprofit_id: charge['nonprofit_id'], + supporter_id: charge['supporter_id'] + }) + + InsertActivities.for_refunds([payment.id]) + + # Update the refund to have the above payment_id + refund.payment = payment + refund.save! + + # Update original payment to increment its refund_total for any future refund attempts + original_payment.refund_total += h['amount'].to_i + original_payment.save! + # Send the refund receipts in a delayed job + + JobQueue.queue JobTypes::RefundCreatedJob, refund + + return {'payment' => payment.attributes, 'refund' => refund.attributes} + end + end + + def self.legacy_refund(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? + + 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']})") + 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['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 + + refund_row = Qx.insert_into(:refunds).values(h.merge(charge_id: charge['id'])).timestamps.returning('*').execute.first + + gross = -(h['amount']) + + 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 + + InsertActivities.for_refunds([payment_row['id']]) + + # Update the refund to have the above payment_id + refund_row = Qx.update(:refunds).set(payment_id: payment_row['id']).ts.where(id: refund_row['id']).returning('*').execute.first + # Update original payment to increment its refund_total for any future refund attempts + Qx.update(:payments).set("refund_total=refund_total + #{h['amount'].to_i}").ts.where(id: original_payment['id']).execute + # 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} + end + + # @param [Hash] opts + # @option opts [Hash] :refund_data the data to pass to the Stripe::Refund#create method + # @option opts [Integer] :nonprofit_id the nonprofit_id that the charge belongs to + # @option opts [Time] :charge_date the time that the charge to be refunded occurred + def self.perform_stripe_refund(opts={}) + refund_data = opts[:refund_data].merge({'reverse_transfer' => true, expand: ['charge']}) + stripe_refund = Stripe::Refund.create(refund_data, {stripe_version: '2019-09-09'}) + stripe_app_fee = Stripe::ApplicationFee.retrieve({id: stripe_refund.charge.application_fee}, {stripe_version: '2019-09-09'}) + fee_to_refund = Nonprofit.find(opts[:nonprofit_id]).calculate_application_fee_refund(refund:stripe_refund, charge:stripe_refund.charge, application_fee:stripe_app_fee, charge_date: opts[:charge_date]) + if fee_to_refund > 0 + app_fee_refund = Stripe::ApplicationFee.create_refund(stripe_refund.charge.application_fee, {amount: fee_to_refund}, {stripe_version: '2019-09-09'}) + end + {stripe_refund: stripe_refund, stripe_app_fee_refund: app_fee_refund} + end +end + diff --git a/lib/insert/insert_source_token.rb b/app/legacy_lib/insert_source_token.rb similarity index 100% rename from lib/insert/insert_source_token.rb rename to app/legacy_lib/insert_source_token.rb diff --git a/app/legacy_lib/insert_supporter.rb b/app/legacy_lib/insert_supporter.rb new file mode 100644 index 000000000..24f3d92bb --- /dev/null +++ b/app/legacy_lib/insert_supporter.rb @@ -0,0 +1,60 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module InsertSupporter + + def self.create_or_update(np_id, data, update=false) + if (BLOCKED_SUPPORTERS.include?(data[:email])) + raise "Blocked supporter" + end + ParamValidation.new(data.merge(np_id: np_id), { + np_id: {required: true, is_integer: true} + }) + address_keys = ['name', 'address', 'city', 'country', 'state_code'] + custom_fields = data['customFields'] + tags = data['tags'] + data = HashWithIndifferentAccess.new(Format::RemoveDiacritics.from_hash(data, address_keys)) + .except(:customFields, :tags) + nonprofit = Nonprofit.find(np_id) + + supporter = nonprofit.supporters.not_deleted.where("name = ? AND email = ?", data[:name], data[:email]).first + if supporter && update + supporter.update(data) + else + supporter = nonprofit.supporters.create(data) + supporter.publish_created + end + + InsertCustomFieldJoins.find_or_create(np_id, [supporter['id']], custom_fields) if custom_fields.present? + InsertTagJoins.find_or_create(np_id, [supporter['id']], tags) if tags.present? + + #GeocodeModel.delay.supporter(supporter['id']) + InsertFullContactInfos.enqueue([supporter['id']]) + + return supporter + end + + # pass in a hash of supporter info, as well as + # any property with tag_x will create a tag with name 'name' + # any property with field_x will create a field with name 'x' and value set + # eg: + # { + # 'name' => 'Bob Ross', + # 'email' => 'bob@happytrees.org', + # 'tag_xy' => true, + # 'field_xy' => 420 + # } + # 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_/)} + 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 + end + +end diff --git a/lib/insert/insert_tag_joins.rb b/app/legacy_lib/insert_tag_joins.rb similarity index 88% rename from lib/insert/insert_tag_joins.rb rename to app/legacy_lib/insert_tag_joins.rb index 11fa284e0..bd9d1feff 100644 --- a/lib/insert/insert_tag_joins.rb +++ b/app/legacy_lib/insert_tag_joins.rb @@ -33,26 +33,26 @@ def self.in_bulk(np_id, profile_id, supporter_ids, tag_data) return {json: {error: "Validation error\n #{e.message}", errors: e.data}, status: :unprocessable_entity} end + tag_data = TagJoin::Modifications.new(tag_data) + 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) - + nonprofit = Nonprofit.find(np_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) + supporter_ids = nonprofit.supporters.where("id IN (?)", supporter_ids).pluck(:id) unless supporter_ids.any? 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 = nonprofit.tag_masters.where("id IN (?)", tag_data.to_tag_master_ids).pluck(:id).to_a + filtered_tag_data = tag_data.for_given_tags(valid_ids) # 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.unselected.to_tag_master_ids deleted = [] if to_remove.any? deleted = Qx.delete_from(:tag_joins) @@ -63,7 +63,7 @@ def self.in_bulk(np_id, profile_id, supporter_ids, tag_data) end # next add only the selected tag_joins - to_insert = filtered_tag_data.select{|t| t[:selected]}.map{|t| t[:tag_master_id]} + to_insert = filtered_tag_data.selected.to_tag_master_ids 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) diff --git a/app/legacy_lib/insert_tickets.rb b/app/legacy_lib/insert_tickets.rb new file mode 100644 index 000000000..4cb509f89 --- /dev/null +++ b/app/legacy_lib/insert_tickets.rb @@ -0,0 +1,265 @@ +# 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: { + # tickets: [{quantity, ticket_level_id}], + # event_id, + # nonprofit_id, + # supporter_id, + # event_discount_id, + # card_id, (if a charge) + # offsite_payment: {kind, check_number}, + # kind (offsite, charge, or free) + # amount: integer + # fee_covered: boolean + # } + def self.create(data, skip_notifications=false) + 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}, + amount: {required: true, is_integer: true} + }) + + data[:tickets].each {|t| + ParamValidation.new(t, {quantity: {is_integer: true, required: true, min: 1}, ticket_level_id: {is_reference: true, required: true}}) + } + + 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.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 + QueryTicketLevels.verify_tickets_available(data[:tickets]) + + estimated_gross_amount = QueryTicketLevels.gross_amount_from_tickets(data[:tickets], data[:event_discount_id]) + gross_amount = data[:amount] + if (gross_amount < estimated_gross_amount) + raise ParamValidation::ValidationError.new("You authorized a payment of $#{Format::Currency.cents_to_dollars(gross_amount)} but the total value of tickets requested was $#{Format::Currency.cents_to_dollars(estimated_gross_amount)}.", key: :amount) + end + + + result = {} + trx = entities[:supporter_id].transactions.build(amount:0, created:Time.current) + tktpur = trx.ticket_purchases.build + if gross_amount > 0 + # Create offsite payment for tickets + if data[:kind] == 'offsite' + current_user = data[:current_user] + # offsite can only come from valid nonprofit users + unless current_user && QueryRoles.is_authorized_for_nonprofit?(current_user.id, entities[:nonprofit_id].id) + raise AuthenticationError + end + + # create payment and offsite payment + result['payment'] = create_payment(entities, gross_amount) + result['offsite_payment'] = create_offsite_payment(entities, gross_amount, data, result['payment']) + + trx.assign_attributes(amount: result['payment'].gross_amount, created: result['payment'].date) + + + legacy_payment = Payment.find(result['payment']['id']) + trx_charge = SubtransactionPayment.new( + legacy_payment: legacy_payment, + paymentable: OfflineTransactionCharge.new, + created: legacy_payment.date + ) + + subtrx = trx.build_subtransaction( + subtransactable: OfflineTransaction.new(amount: result['payment'].gross_amount), + subtransaction_payments:[ + trx_charge + ]) + # Create charge for tickets + elsif data['kind'] == 'charge' || !data['kind'] + source_token = QuerySourceToken.get_and_increment_source_token(data[:token],nil) + QuerySourceToken.validate_source_token_type(source_token) + tokenizable = source_token.tokenizable + + unless entities[:nonprofit_id].can_process_charge? + raise ParamValidation::ValidationError.new("Nonprofit #{entities[:nonprofit_id].id} is not allowed to process charges", key: :nonprofit_id) + end + + ## 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, + fee_covered:data[:fee_covered] + })) + if result['charge']['status'] == 'failed' + raise ChargeError.new(result['charge']['failure_message']) + end + + trx.assign_attributes(amount: result['payment'].gross_amount, created: result['payment'].date) + + legacy_payment = Payment.find(result['payment']['id']) + trx_charge = SubtransactionPayment.new( + legacy_payment: legacy_payment, + paymentable: StripeTransactionCharge.new, + created: legacy_payment.date + ) + + subtrx = trx.build_subtransaction( + subtransactable: StripeTransaction.new(amount: result['payment'].gross_amount), + subtransaction_payments:[ + trx_charge + ]) + else + raise ParamValidation::ValidationError.new("Ticket costs money but you didn't pay.", {key: :kind}) + end + end + + # Generate the bid ids + data['tickets'] = generate_bid_ids(entities[:event_id].id, tl_entities) + + result['tickets'] = generated_ticket_entities(data['tickets'], result, entities) + + tktpur.tickets = result['tickets'] + + trx.save! + tktpur.save! + if subtrx + subtrx.save! + subtrx.subtransaction_payments.each(&:publish_created) + end + + #tktpur.publish_created + trx.publish_created + + # Create the activity rows for the tickets + InsertActivities.for_tickets(result['tickets'].map{|t| t.id}) + + ticket_ids = result['tickets'].map{|t| t.id} + charge_id = result['charge'] ? result['charge'].id : nil + + unless skip_notifications + JobQueue.queue(JobTypes::TicketMailerReceiptAdminJob, ticket_ids) + JobQueue.queue(JobTypes::TicketMailerFollowupJob, ticket_ids, charge_id) + end + + return 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 = Ticket.where(event_id: event_id)&.pluck(:bid_id)&.max || 0 + 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 + def self.generated_ticket_entities(ticket_data, result, entities) + ticket_data.map{|ticket_request| + t = Ticket.new + t.quantity = ticket_request['quantity'] + t.ticket_level = ticket_request['ticket_level_id'] + t.event = entities[:event_id] + t.supporter = entities[:supporter_id] + t.payment = result['payment'] + t.charge = result['charge'] + t.bid_id = ticket_request['bid_id'] + t.event_discount = entities[:event_discount_id] + t.save! + t + }.to_a + end + + def self.validate_entities(entities, tl_entities) + ## is supporter deleted? If supporter is deleted, we error! + if entities[:supporter_id].deleted + raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} is deleted", key: :supporter_id) + end + + if entities[:event_id].deleted + raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} is deleted", key: :event_id) + end + + #verify that enough tickets_available + tl_entities.each {|i| + if i[:ticket_level_id].deleted + raise ParamValidation::ValidationError.new("Ticket level #{i[:ticket_level_id].id} is deleted", key: :tickets) + end + + 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 + } + + # Does the supporter belong to the nonprofit? + if entities[:supporter_id].nonprofit != entities[:nonprofit_id] + raise ParamValidation::ValidationError.new("Supporter #{entities[:supporter_id].id} does not belong to nonprofit #{entities[:nonprofit_id].id}", key: :supporter_id) + end + + ## does event belong to nonprofit + if entities[:event_id].nonprofit != entities[:nonprofit_id] + raise ParamValidation::ValidationError.new("Event #{entities[:event_id].id} does not belong to nonprofit #{entities[:nonprofit_id]}", key: :event_id) + end + + if entities[:event_discount_id] && entities[:event_discount_id].event != entities[:event_id] + raise ParamValidation::ValidationError.new("Event discount #{entities[:event_discount_id].id} does not belong to event #{entities[:event_id].id}", key: :event_discount_id) + end + end + + def self.get_ticket_level_entities(data) + data[:tickets].map{|i| + { + quantity: i[:quantity], + ticket_level_id: RetrieveActiveRecordItems.retrieve_from_keys(i, TicketLevel => :ticket_level_id)[:ticket_level_id] + } + }.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.date = Time.current + p.towards = entities[:event_id].name + p.fee_total = 0 + p.net_amount = gross_amount + 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.payment = payment + p.kind = data['offsite_payment']['kind'] + p.check_number = data['offsite_payment']['check_number'] + p.save! + p + end +end diff --git a/lib/insert/insert_tracking.rb b/app/legacy_lib/insert_tracking.rb similarity index 100% rename from lib/insert/insert_tracking.rb rename to app/legacy_lib/insert_tracking.rb diff --git a/app/legacy_lib/interpolation_dictionary.rb b/app/legacy_lib/interpolation_dictionary.rb new file mode 100644 index 000000000..70fec351a --- /dev/null +++ b/app/legacy_lib/interpolation_dictionary.rb @@ -0,0 +1,33 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +# InterpolationDictionary is a simple class for replacing braced variables, +# like {{NAME}}, with a different value. We use this for email templates. +class InterpolationDictionary + attr_reader :entries + + # pass in entries with defaults + def initialize(entries={}) + @entries = entries + end + + def add_entry(entry_name, value) + if @entries.has_key?(entry_name) && full_sanitize(value).present? + @entries[entry_name] = full_sanitize(value) + end + end + + def interpolate(message) + result = Format::Interpolate.with_hash(message, @entries) + sanitize(result) if sanitize(result).present? + end + + private + + def full_sanitize(value) + ActionView::Base.full_sanitizer.sanitize(value) + end + + def sanitize(value) + ActionView::Base.white_list_sanitizer.sanitize(value) + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_queue.rb b/app/legacy_lib/job_queue.rb new file mode 100644 index 000000000..bda8d6d29 --- /dev/null +++ b/app/legacy_lib/job_queue.rb @@ -0,0 +1,6 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobQueue + def self.queue(klass, *args) + Delayed::Job.enqueue klass.new(*args) + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_failed_gift_job.rb b/app/legacy_lib/job_types/admin_failed_gift_job.rb new file mode 100644 index 000000000..35a9b6fd0 --- /dev/null +++ b/app/legacy_lib/job_types/admin_failed_gift_job.rb @@ -0,0 +1,16 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminFailedGiftJob < EmailJob + attr_reader :donation, :campaign_gift_option, :payment + + def initialize(donation, payment, campaign_gift_option) + @donation = donation + @payment = payment + @campaign_gift_option = campaign_gift_option + end + + def perform + AdminMailer.notify_failed_gift(@donation, @payment, @campaign_gift_option).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_created_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_created_job.rb new file mode 100644 index 000000000..6dddb81e6 --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_created_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeCreatedJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.created(dispute).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_funds_reinstated_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_funds_reinstated_job.rb new file mode 100644 index 000000000..387b82d1e --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_funds_reinstated_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeFundsReinstatedJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.funds_reinstated(dispute).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_funds_withdrawn_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_funds_withdrawn_job.rb new file mode 100644 index 000000000..d0a7dc242 --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_funds_withdrawn_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeFundsWithdrawnJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.funds_withdrawn(dispute).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_lost_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_lost_job.rb new file mode 100644 index 000000000..f3a1b748f --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_lost_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeLostJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.lost(dispute).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_updated_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_updated_job.rb new file mode 100644 index 000000000..c7e01582d --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_updated_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeUpdatedJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.updated(dispute).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/admin_notice_dispute_won_job.rb b/app/legacy_lib/job_types/admin_notice_dispute_won_job.rb new file mode 100644 index 000000000..1bec960f1 --- /dev/null +++ b/app/legacy_lib/job_types/admin_notice_dispute_won_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class AdminNoticeDisputeWonJob < EmailJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + DisputeMailer.won(dispute).deliver + end + end +end \ No newline at end of file diff --git a/lib/job_types/admin_notice_job.rb b/app/legacy_lib/job_types/admin_notice_job.rb similarity index 100% rename from lib/job_types/admin_notice_job.rb rename to app/legacy_lib/job_types/admin_notice_job.rb diff --git a/lib/job_types/campaign_creation_followup_job.rb b/app/legacy_lib/job_types/campaign_creation_followup_job.rb similarity index 100% rename from lib/job_types/campaign_creation_followup_job.rb rename to app/legacy_lib/job_types/campaign_creation_followup_job.rb diff --git a/app/legacy_lib/job_types/campaign_updated_job.rb b/app/legacy_lib/job_types/campaign_updated_job.rb new file mode 100644 index 000000000..b4080d093 --- /dev/null +++ b/app/legacy_lib/job_types/campaign_updated_job.rb @@ -0,0 +1,20 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class CampaignUpdatedJob < GenericJob + attr_reader :campaign_id + + def initialize(campaign_id) + @campaign_id = campaign_id + end + + def campaign + Campaign.find(@campaign_id) + end + + def perform + campaign.children_campaigns.each do |child| + JobQueue.queue(JobTypes::ChildCampaignUpdateJob, child) + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/child_campaign_update_job.rb b/app/legacy_lib/job_types/child_campaign_update_job.rb new file mode 100644 index 000000000..c32fc273a --- /dev/null +++ b/app/legacy_lib/job_types/child_campaign_update_job.rb @@ -0,0 +1,18 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class ChildCampaignUpdateJob < GenericJob + attr_reader :child_campaign_id + + def initialize(child_campaign_id) + @child_campaign_id = child_campaign_id + end + + def child_campaign + Campaign.find(child_campaign_id) + end + + def perform + child_campaign.update_from_parent! + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_created_job.rb b/app/legacy_lib/job_types/dispute_created_job.rb new file mode 100644 index 000000000..57412546b --- /dev/null +++ b/app/legacy_lib/job_types/dispute_created_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeCreatedJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeCreatedJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_funds_reinstated_job.rb b/app/legacy_lib/job_types/dispute_funds_reinstated_job.rb new file mode 100644 index 000000000..1a6638360 --- /dev/null +++ b/app/legacy_lib/job_types/dispute_funds_reinstated_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeFundsReinstatedJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeFundsReinstatedJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_funds_withdrawn_job.rb b/app/legacy_lib/job_types/dispute_funds_withdrawn_job.rb new file mode 100644 index 000000000..2460fd6bd --- /dev/null +++ b/app/legacy_lib/job_types/dispute_funds_withdrawn_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeFundsWithdrawnJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeFundsWithdrawnJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_lost_job.rb b/app/legacy_lib/job_types/dispute_lost_job.rb new file mode 100644 index 000000000..e109c3743 --- /dev/null +++ b/app/legacy_lib/job_types/dispute_lost_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeLostJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeLostJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_updated_job.rb b/app/legacy_lib/job_types/dispute_updated_job.rb new file mode 100644 index 000000000..30cb0af69 --- /dev/null +++ b/app/legacy_lib/job_types/dispute_updated_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeUpdatedJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeUpdatedJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/dispute_won_job.rb b/app/legacy_lib/job_types/dispute_won_job.rb new file mode 100644 index 000000000..fc898bebd --- /dev/null +++ b/app/legacy_lib/job_types/dispute_won_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DisputeWonJob < GenericJob + attr_reader :dispute + + def initialize(dispute) + @dispute = dispute + end + + def perform + JobQueue.queue(JobTypes::AdminNoticeDisputeWonJob, dispute) + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/donation_payment_create_job.rb b/app/legacy_lib/job_types/donation_payment_create_job.rb new file mode 100644 index 000000000..f40cccef6 --- /dev/null +++ b/app/legacy_lib/job_types/donation_payment_create_job.rb @@ -0,0 +1,18 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class DonationPaymentCreateJob < GenericJob + attr_reader :donation_id, :locale, :payment_id + + def initialize(donation_id, payment_id, locale=I18n.locale) + @donation_id = donation_id + @payment_id = payment_id + @locale = locale + end + + def perform + JobQueue.queue(JobTypes::DonorPaymentNotificationJob, donation_id, payment_id, locale) + JobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, donation_id, payment_id) + JobQueue.queue(JobTypes::NonprofitFirstDonationPaymentJob, donation_id) + end + end +end \ No newline at end of file diff --git a/lib/job_types/donor_direct_debit_notification_job.rb b/app/legacy_lib/job_types/donor_direct_debit_notification_job.rb similarity index 100% rename from lib/job_types/donor_direct_debit_notification_job.rb rename to app/legacy_lib/job_types/donor_direct_debit_notification_job.rb diff --git a/lib/job_types/donor_failed_recurring_donation_job.rb b/app/legacy_lib/job_types/donor_failed_recurring_donation_job.rb similarity index 100% rename from lib/job_types/donor_failed_recurring_donation_job.rb rename to app/legacy_lib/job_types/donor_failed_recurring_donation_job.rb diff --git a/app/legacy_lib/job_types/donor_payment_notification_job.rb b/app/legacy_lib/job_types/donor_payment_notification_job.rb new file mode 100644 index 000000000..96990f8d3 --- /dev/null +++ b/app/legacy_lib/job_types/donor_payment_notification_job.rb @@ -0,0 +1,15 @@ +# 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, :payment_id + def initialize(donation_id, payment_id, locale=I18n.locale) + @donation_id = donation_id + @payment_id = payment_id + @locale = locale + end + + def perform + DonationMailer.donor_payment_notification(@donation_id, @payment_id, @locale).deliver + end + end +end \ No newline at end of file diff --git a/lib/job_types/donor_recurring_donation_change_amount_job.rb b/app/legacy_lib/job_types/donor_recurring_donation_change_amount_job.rb similarity index 100% rename from lib/job_types/donor_recurring_donation_change_amount_job.rb rename to app/legacy_lib/job_types/donor_recurring_donation_change_amount_job.rb diff --git a/lib/job_types/donor_refund_notification_job.rb b/app/legacy_lib/job_types/donor_refund_notification_job.rb similarity index 100% rename from lib/job_types/donor_refund_notification_job.rb rename to app/legacy_lib/job_types/donor_refund_notification_job.rb diff --git a/lib/job_types/email_job.rb b/app/legacy_lib/job_types/email_job.rb similarity index 93% rename from lib/job_types/email_job.rb rename to app/legacy_lib/job_types/email_job.rb index e5811637f..214f5da3a 100644 --- a/lib/job_types/email_job.rb +++ b/app/legacy_lib/job_types/email_job.rb @@ -14,6 +14,7 @@ def destroy_failed_jobs? end def error(job, exception) + Airbrake.notify(exception) end def reschedule_at(current_time, attempts) diff --git a/lib/job_types/event_creation_followup_job.rb b/app/legacy_lib/job_types/event_creation_followup_job.rb similarity index 100% rename from lib/job_types/event_creation_followup_job.rb rename to app/legacy_lib/job_types/event_creation_followup_job.rb diff --git a/lib/job_types/export_payment_completed_job.rb b/app/legacy_lib/job_types/export_payment_completed_job.rb similarity index 100% rename from lib/job_types/export_payment_completed_job.rb rename to app/legacy_lib/job_types/export_payment_completed_job.rb diff --git a/lib/job_types/export_payment_failed_job.rb b/app/legacy_lib/job_types/export_payment_failed_job.rb similarity index 100% rename from lib/job_types/export_payment_failed_job.rb rename to app/legacy_lib/job_types/export_payment_failed_job.rb diff --git a/lib/job_types/export_recurring_donations_completed_job.rb b/app/legacy_lib/job_types/export_recurring_donations_completed_job.rb similarity index 100% rename from lib/job_types/export_recurring_donations_completed_job.rb rename to app/legacy_lib/job_types/export_recurring_donations_completed_job.rb diff --git a/lib/job_types/export_recurring_donations_failed_job.rb b/app/legacy_lib/job_types/export_recurring_donations_failed_job.rb similarity index 100% rename from lib/job_types/export_recurring_donations_failed_job.rb rename to app/legacy_lib/job_types/export_recurring_donations_failed_job.rb diff --git a/lib/job_types/export_supporter_notes_completed_job.rb b/app/legacy_lib/job_types/export_supporter_notes_completed_job.rb similarity index 100% rename from lib/job_types/export_supporter_notes_completed_job.rb rename to app/legacy_lib/job_types/export_supporter_notes_completed_job.rb diff --git a/lib/job_types/export_supporter_notes_failed_job.rb b/app/legacy_lib/job_types/export_supporter_notes_failed_job.rb similarity index 100% rename from lib/job_types/export_supporter_notes_failed_job.rb rename to app/legacy_lib/job_types/export_supporter_notes_failed_job.rb diff --git a/lib/job_types/export_supporters_completed_job.rb b/app/legacy_lib/job_types/export_supporters_completed_job.rb similarity index 100% rename from lib/job_types/export_supporters_completed_job.rb rename to app/legacy_lib/job_types/export_supporters_completed_job.rb diff --git a/lib/job_types/export_supporters_failed_job.rb b/app/legacy_lib/job_types/export_supporters_failed_job.rb similarity index 100% rename from lib/job_types/export_supporters_failed_job.rb rename to app/legacy_lib/job_types/export_supporters_failed_job.rb diff --git a/app/legacy_lib/job_types/generic_job.rb b/app/legacy_lib/job_types/generic_job.rb new file mode 100644 index 000000000..0c90a5826 --- /dev/null +++ b/app/legacy_lib/job_types/generic_job.rb @@ -0,0 +1,28 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class GenericJob + def perform + raise 'You need to override this' + end + + def max_attempts + MAX_GENERIC_JOB_ATTEMPTS || 1 + end + + def destroy_failed_jobs? + false + end + + def error(job, exception) + Airbrake.notify(exception) + end + + def reschedule_at(current_time, attempts) + current_time + attempts**(2.195); + end + + def queue_name + 'generic_queue' + end + end +end \ No newline at end of file diff --git a/lib/job_types/generic_mail_job.rb b/app/legacy_lib/job_types/generic_mail_job.rb similarity index 100% rename from lib/job_types/generic_mail_job.rb rename to app/legacy_lib/job_types/generic_mail_job.rb diff --git a/lib/job_types/import_complete_notification_job.rb b/app/legacy_lib/job_types/import_complete_notification_job.rb similarity index 100% rename from lib/job_types/import_complete_notification_job.rb rename to app/legacy_lib/job_types/import_complete_notification_job.rb diff --git a/lib/job_types/nonprofit_admin_existing_invite_job.rb b/app/legacy_lib/job_types/nonprofit_admin_existing_invite_job.rb similarity index 100% rename from lib/job_types/nonprofit_admin_existing_invite_job.rb rename to app/legacy_lib/job_types/nonprofit_admin_existing_invite_job.rb diff --git a/lib/job_types/nonprofit_admin_new_invite_job.rb b/app/legacy_lib/job_types/nonprofit_admin_new_invite_job.rb similarity index 100% rename from lib/job_types/nonprofit_admin_new_invite_job.rb rename to app/legacy_lib/job_types/nonprofit_admin_new_invite_job.rb diff --git a/lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb b/app/legacy_lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb similarity index 100% rename from lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb rename to app/legacy_lib/job_types/nonprofit_admin_supporter_fundraiser_job.rb diff --git a/app/legacy_lib/job_types/nonprofit_create_job.rb b/app/legacy_lib/job_types/nonprofit_create_job.rb new file mode 100644 index 000000000..d046d4788 --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_create_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class NonprofitCreateJob < GenericJob + attr_reader :nonprofit_id + + def initialize(nonprofit_id) + @nonprofit_id = nonprofit_id + end + + def perform + Delayed::Job.enqueue JobTypes::NonprofitWelcomeJob.new nonprofit_id + end + end +end \ No newline at end of file diff --git a/lib/job_types/nonprofit_failed_recurring_donation_job.rb b/app/legacy_lib/job_types/nonprofit_failed_recurring_donation_job.rb similarity index 100% rename from lib/job_types/nonprofit_failed_recurring_donation_job.rb rename to app/legacy_lib/job_types/nonprofit_failed_recurring_donation_job.rb diff --git a/app/legacy_lib/job_types/nonprofit_first_charge_email_job.rb b/app/legacy_lib/job_types/nonprofit_first_charge_email_job.rb new file mode 100644 index 000000000..ef74e2945 --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_first_charge_email_job.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class NonprofitFirstChargeEmailJob < EmailJob + attr_reader :nonprofit_id + + def initialize(nonprofit_id) + @nonprofit_id = nonprofit_id + end + + def perform + NonprofitMailer.first_charge_email(nonprofit_id).deliver + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/nonprofit_first_donation_payment_job.rb b/app/legacy_lib/job_types/nonprofit_first_donation_payment_job.rb new file mode 100644 index 000000000..83c926119 --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_first_donation_payment_job.rb @@ -0,0 +1,25 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class NonprofitFirstDonationPaymentJob < GenericJob + attr_reader :donation_id + + def initialize(donation_id) + @donation_id = donation_id + end + + def perform + d = Donation.find(donation_id) + nonprofit = d.nonprofit + if nonprofit && d.charges.any? + np_infos = nonprofit.miscellaneous_np_info || nonprofit.create_miscellaneous_np_info + np_infos.with_lock("FOR UPDATE") do + if !np_infos.first_charge_email_sent + JobQueue.queue(JobTypes::NonprofitFirstChargeEmailJob, nonprofit.id) + np_infos.first_charge_email_sent = true + np_infos.save! + end + end + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/job_types/nonprofit_first_ticket_payment_job.rb b/app/legacy_lib/job_types/nonprofit_first_ticket_payment_job.rb new file mode 100644 index 000000000..9c2c7a637 --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_first_ticket_payment_job.rb @@ -0,0 +1,25 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class NonprofitFirstTicketPaymentJob < GenericJob + attr_reader :tickets_id + + def initialize(ticket_ids) + @ticket_ids = ticket_ids + end + + def perform + ticket = Ticket.find(@ticket_ids.first) + nonprofit = ticket.event&.nonprofit + if nonprofit && ticket.charge + np_infos = nonprofit.miscellaneous_np_info || nonprofit.create_miscellaneous_np_info + np_infos.with_lock("FOR UPDATE") do + if !np_infos.first_charge_email_sent + JobQueue.queue(JobTypes::NonprofitFirstChargeEmailJob, nonprofit.id) + np_infos.first_charge_email_sent = true + np_infos.save! + end + end + end + end + end +end \ No newline at end of file diff --git a/lib/job_types/nonprofit_new_bank_account_job.rb b/app/legacy_lib/job_types/nonprofit_new_bank_account_job.rb similarity index 100% rename from lib/job_types/nonprofit_new_bank_account_job.rb rename to app/legacy_lib/job_types/nonprofit_new_bank_account_job.rb diff --git a/app/legacy_lib/job_types/nonprofit_payment_notification_job.rb b/app/legacy_lib/job_types/nonprofit_payment_notification_job.rb new file mode 100644 index 000000000..b57c91a5f --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_payment_notification_job.rb @@ -0,0 +1,15 @@ +# 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, :payment_id + def initialize(donation_id, payment_id, user_id=nil) + @donation_id = donation_id + @payment_id = payment_id + @user_id = user_id + end + + def perform + DonationMailer.nonprofit_payment_notification(@donation_id, @payment_id, @user_id).deliver + end + end +end \ No newline at end of file diff --git a/lib/job_types/nonprofit_pending_payout_job.rb b/app/legacy_lib/job_types/nonprofit_pending_payout_job.rb similarity index 100% rename from lib/job_types/nonprofit_pending_payout_job.rb rename to app/legacy_lib/job_types/nonprofit_pending_payout_job.rb diff --git a/lib/job_types/nonprofit_recurring_donation_cancellation_job.rb b/app/legacy_lib/job_types/nonprofit_recurring_donation_cancellation_job.rb similarity index 100% rename from lib/job_types/nonprofit_recurring_donation_cancellation_job.rb rename to app/legacy_lib/job_types/nonprofit_recurring_donation_cancellation_job.rb diff --git a/lib/job_types/nonprofit_recurring_donation_change_amount_job.rb b/app/legacy_lib/job_types/nonprofit_recurring_donation_change_amount_job.rb similarity index 100% rename from lib/job_types/nonprofit_recurring_donation_change_amount_job.rb rename to app/legacy_lib/job_types/nonprofit_recurring_donation_change_amount_job.rb diff --git a/app/legacy_lib/job_types/nonprofit_refund_notification_job.rb b/app/legacy_lib/job_types/nonprofit_refund_notification_job.rb new file mode 100644 index 000000000..8678de7c6 --- /dev/null +++ b/app/legacy_lib/job_types/nonprofit_refund_notification_job.rb @@ -0,0 +1,14 @@ +# 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, :user_id + def initialize(refund_id, user_id=nil) + @refund_id = refund_id + @user_id = user_id + end + + def perform + NonprofitMailer.refund_notification(@refund_id, @user_id).deliver + end + end +end \ No newline at end of file diff --git a/lib/job_types/nonprofit_welcome_job.rb b/app/legacy_lib/job_types/nonprofit_welcome_job.rb similarity index 100% rename from lib/job_types/nonprofit_welcome_job.rb rename to app/legacy_lib/job_types/nonprofit_welcome_job.rb diff --git a/app/legacy_lib/job_types/refund_created_job.rb b/app/legacy_lib/job_types/refund_created_job.rb new file mode 100644 index 000000000..6d39942f0 --- /dev/null +++ b/app/legacy_lib/job_types/refund_created_job.rb @@ -0,0 +1,15 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module JobTypes + class RefundCreatedJob < GenericJob + attr_reader :refund + + def initialize(refund) + @refund = refund + end + + def perform + JobQueue.queue JobTypes::DonorRefundNotificationJob, refund.id + JobQueue.queue JobTypes::NonprofitRefundNotificationJob, refund.id + end + end +end \ No newline at end of file diff --git a/lib/job_types/ticket_mailer_followup_job.rb b/app/legacy_lib/job_types/ticket_mailer_followup_job.rb similarity index 100% rename from lib/job_types/ticket_mailer_followup_job.rb rename to app/legacy_lib/job_types/ticket_mailer_followup_job.rb diff --git a/lib/job_types/ticket_mailer_receipt_admin_job.rb b/app/legacy_lib/job_types/ticket_mailer_receipt_admin_job.rb similarity index 100% rename from lib/job_types/ticket_mailer_receipt_admin_job.rb rename to app/legacy_lib/job_types/ticket_mailer_receipt_admin_job.rb diff --git a/lib/json_resp.rb b/app/legacy_lib/json_resp.rb similarity index 95% rename from lib/json_resp.rb rename to app/legacy_lib/json_resp.rb index d5dd39b04..f4d323f0f 100644 --- a/lib/json_resp.rb +++ b/app/legacy_lib/json_resp.rb @@ -124,12 +124,12 @@ def as_date end def min(n) - @errors.concat @keys.reject{|k| @params[k] >= n}.map{|k| "#{k} must be at least #{n}"} + @errors.concat @keys.reject{|k| @params[k].to_i >= n}.map{|k| "#{k} must be at least #{n}"} return self end def max(n) - @errors.concat @keys.reject{|k| @params[k] <= n}.map{|k| "#{k} must be less than #{n + 1}"} + @errors.concat @keys.reject{|k| @params[k].to_i <= n}.map{|k| "#{k} must be less than #{n + 1}"} return self end diff --git a/app/legacy_lib/mailchimp.rb b/app/legacy_lib/mailchimp.rb new file mode 100644 index 000000000..ebc34aaf2 --- /dev/null +++ b/app/legacy_lib/mailchimp.rb @@ -0,0 +1,283 @@ +# 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 + logger Rails.logger, :info, :mailchimp + + def self.base_uri(key) + dc = get_datacenter(key) + return "https://#{dc}.api.mailchimp.com/3.0" + end + + # Run the configuration from an initializer + # data: {:api_key => String} + def self.config(hash) + @apikey = hash[:api_key] + @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}", + 'apikey' => @apikey + }, + logger: Rails.logger, + log_level: :info, + log_format: :mailchimp + }) + return metadata['dc'] + end + + def self.signup supporter, mailchimp_list + body_hash = @body.merge(create_subscribe_body(supporter)) + put(mailchimp_list.list_members_url, @options.merge(:body => body_hash.to_json)) + end + + def self.signup_nonprofit_user(drip_email_list, nonprofit, user) + body_hash = @body.merge(create_nonprofit_user_subscribe_body(nonprofit, user)) + uri = "https://us5.api.mailchimp.com/3.0" # hardcoded for us + put(uri + "/" + generate_list_member_path(drip_email_list.list_members_path, user.email), @options.merge(:body => body_hash.to_json, + basic_auth: {username: "user", password: @apikey})) + 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 + end + + # Get all lists owned by the nonprofit represented by the mailchimp token + def get_all_lists(mailchimp_token) + uri = base_uri(mailchimp_token) + puts "URI #{uri}" + puts "KEY #{mailchimp_token}" + get(uri+'/lists', { + basic_auth: {username: '', password: mailchimp_token}, + headers: {'Content-Type' => 'application/json'}, + } + ) + 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) + 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.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: 'US', + 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@commitchange.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']} + end + end + + # Given a nonprofit id and post_data, which is an array of batch operation hashes OR MailchimpBatchOperation objects + # See here: http://developer.mailchimp.com/documentation/mailchimp/guides/how-to-use-batch-operations/ + # Perform all the batch operations and return a status report + def self.perform_batch_operations(npo_id, post_data) + post_data = post_data.map(&:to_h).select(&:present?) # the select removes any nil items + 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: @username, 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: @username, 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}}) + 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 = get_emails_for_supporter_ids(npo_id, supporter_ids) + to_add = get_mailchimp_list_ids(tag_data.selected.to_tag_master_ids) + to_remove = get_mailchimp_list_ids(tag_data.unselected.to_tag_master_ids) + 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 + perform_batch_operations(npo_id, bulk_post.concat(bulk_delete)) + end + + def self.get_emails_for_supporter_ids(npo_id, supporters_ids=[]) + Nonprofit.find(npo_id).supporters.where('id in (?)', supporters_ids).pluck(:email).select(&:present?) + 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 + + + # @param [Nonprofit] nonprofit + # @param [Boolean] delete_from_mailchimp do you want to delete extra items on mailchimp, defaults to false + def self.hard_sync_lists(nonprofit, delete_from_mailchimp=false) + return if !nonprofit + + nonprofit.tag_masters.not_deleted.each do |i| + if (i.email_list) + hard_sync_list(i.email_list, delete_from_mailchimp) + end + end + end + + def self.sync_nonprofit_users + User.nonprofit_personnel.find_each do |np_user| + MailchimpNonprofitUserAddJob.perform_later(np_user, np_user.roles.nonprofit_personnel.first.host ) + end + end + + # @param [EmailList] email_list + # @param [Boolean] delete_from_mailchimp do you want to delete extra items on mailchimp, defaults to false + def self.hard_sync_list(email_list, delete_from_mailchimp=false) + ops = generate_batch_ops_for_hard_sync(email_list, delete_from_mailchimp) + perform_batch_operations(email_list.nonprofit.id, ops) + + end + + # @param [Boolean] delete_from_mailchimp do you want to delete extra items on mailchimp, defaults to false + def self.generate_batch_ops_for_hard_sync(email_list, delete_from_mailchimp=false) + #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} + + #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} + end + + _, in_our_side_only = our_supporters.partition do |s| + mailchimp_subscribers.any?{|mc_sub| s.email.downcase == mc_sub[:email_address].downcase} + 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: create_subscribe_body(i).to_json} + } + + if delete_from_mailchimp + # 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]}"}}) + end + + return 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: @username, 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?count=1000000000", { + basic_auth: {username: @username, password: mailchimp_token}, + headers: {'Content-Type' => 'application/json'}}) + result['lists'] + + end + + def self.get_list(nonprofit, list_id) + mailchimp_token = get_mailchimp_token(nonprofit.id) + uri = base_uri(mailchimp_token) + result = get(uri + "/lists/#{list_id}", { + basic_auth: {username: @username, password: mailchimp_token}, + headers: {'Content-Type' => 'application/json'}}) + result + end + + def self.create_nonprofit_user_subscribe_body(nonprofit,user) + JSON::parse(ApplicationController.render 'mailchimp/nonprofit_user_subscribe', assigns: {nonprofit: nonprofit, user: user }) + end + + def self.create_subscribe_body(supporter) + JSON::parse(ApplicationController.render 'mailchimp/list', assigns: {supporter: supporter}) + end + + def self.generate_list_member_path(list_members_path, email) + list_members_path + "/" + Digest::MD5.hexdigest(email.downcase) + end +end diff --git a/lib/maintain/maintain_dedications.rb b/app/legacy_lib/maintain_dedications.rb similarity index 94% rename from lib/maintain/maintain_dedications.rb rename to app/legacy_lib/maintain_dedications.rb index d58ae687d..593352076 100644 --- a/lib/maintain/maintain_dedications.rb +++ b/app/legacy_lib/maintain_dedications.rb @@ -16,9 +16,9 @@ def self.retrieve_non_json_dedications(include_blank=false) def self.create_json_dedications_from_plain_text(dedications) dedications.map do |i| output = {id: i['id']} - if i['dedication'] =~ /(((in (loving )?)?memory of|in memorium)\:? )(.+)/i + if i['dedication'] =~ /(((in (loving )?)?memory of|in memorium)\:? )(.+)/i || i['dedication'] =~ /(IMO )(.+)/ output[:dedication] = JSON.generate({type: 'memory', note: $+ }) - elsif i['dedication'] =~ /((in honor of|honor of)\:? )(.+)/i + elsif i['dedication'] =~ /((in honor of|honor of)\:? )(.+)/i || i['dedication'] =~ /(IHO )(.+)/ output[:dedication] = JSON.generate({type: 'honor', note: $+ }) else output[:dedication] = JSON.generate({type: 'honor', note: i['dedication'] }) diff --git a/app/legacy_lib/maintain_donation_validity.rb b/app/legacy_lib/maintain_donation_validity.rb new file mode 100644 index 000000000..24e5ed599 --- /dev/null +++ b/app/legacy_lib/maintain_donation_validity.rb @@ -0,0 +1,195 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module MaintainDonationValidity + + # some tickets have invalid records. Find them. + def self.get_invalid_donations + invalid = [] + + Donation.includes( {:supporter => :nonprofit}, {:payment => :supporter }, :nonprofit, :campaign_gifts, :campaign).find_each(batch_size: 10000) do | d| + + donation = {donation:d, issues:[]} + + first_level(donation) + + if (donation[:issues].any?) + invalid.push(donation) + else + second_level(donation) + if (donation[:issues].any?) + invalid.push(donation) + end + end + end + + # second_level(tickets) + # a, tickets = tickets.partition{|i| i[:issues].any?} + # invalid = invalid.concat(a) + invalid + end + + # some tickets have valid records, format a report of them + def self.report(invalid_records) + output = invalid_records.map{|d| + donation = d[:donation] + { + donation_id: donation.id, + donation_nonprofit_id: donation.nonprofit_id, + donation_nonprofit_name: donation.nonprofit&.name, + supporter_id: donation.supporter_id, + supporter_name: donation.supporter&.name, + supporter_nonprofit_id: donation.supporter&.nonprofit_id, + supporter_nonprofit_name: donation.supporter&.nonprofit&.name, + payment_id: donation.payment_id, + payment_supporter_id: donation.payment&.supporter_id, + donation_date: donation.created_at, + donation_card_stripe_id: donation.card&.stripe_customer_id, + donation_card_holder: donation.card&.holder_id, + donation_campaign_id: donation.campaign_id, + donation_campaign_exists: !donation.campaign.nil?, + donation_campaign_gift: donation.campaign_gifts.map{|i| i.id}.join(', '), + donation_recurring_donation_active: donation.recurring_donation&.active, + donation_recurring_donation_failures: donation.recurring_donation&.n_failures, + errors: d[:issues] + } + } + end + + def self.has_no_supporter(t) + if !t[:donation].supporter + t[:issues].push(:no_supporter) + end + end + + def self.has_no_nonprofit(t) + if !t[:donation].nonprofit + t[:issues].push(:no_nonprofit) + end + end + + def self.first_level(d) + has_no_nonprofit(d) + has_no_supporter(d) + end + + def self.donation_and_supporter_no_match(t) + if t[:donation].nonprofit != t[:donation].supporter&.nonprofit + t[:issues].push(:donation_and_supporter_nps_dont_match) + end + end + + def self.payment_and_supporter_no_match(t) + if t[:donation].payment && (t[:donation].payment&.supporter != t[:donation].supporter) + t[:issues].push(:payment_and_donation_supporter_no_match) + end + end + + def self.second_level(donation) + donation_and_supporter_no_match(donation) + payment_and_supporter_no_match(donation) + end + + # some donations have invalid records. Clean them up. + def self.cleanup(invalid_donations) + Qx.transaction do + invalid_donations.each do |d| + + if d[:issues].include?(:no_supporter) + cleanup_for_no_supporter(d[:donation]) + end + + if d[:issues].include?(:donation_and_supporter_nps_dont_match) + cleanup_for_donation_and_supporter_nps_dont_match(d[:donation]) + end + + if d[:issues].include?(:no_nonprofit) + cleanup_for_no_nonprofit(d[:donation]) + end + end + end + end + + def self.cleanup_for_no_supporter(donation) + np = donation.nonprofit + if (np && !Supporter.exists?(donation.supporter_id)) + if (donation.payment&.supporter && donation.payment.supporter.nonprofit == np) + donation.supporter = donation.payment.supporter + donation.save! + elsif (!donation.payment&.supporter) + + supporter = np.supporters.build + supporter.deleted = true + if (donation.supporter_id) + supporter.id = donation.supporter_id + end + supporter.save! + + if (!donation.supporter_id) + donation.supporter = supporter + donation.save! + end + end + end + end + + def self.cleanup_for_no_nonprofit(donation) + if (!donation.nonprofit && !donation.supporter && !donation.recurring_donation && !donation.campaign && (!donation.payment || !donation.payment.nonprofit) && donation.campaign_gifts.none? && donation.activities.none?) + if (donation.payment) + donation.payment.destroy + end + donation.destroy + end + end + + def self.cleanup_for_donation_and_supporter_nps_dont_match(donation) + if (!donation.supporter.nonprofit && donation.nonprofit) + donation.supporter.nonprofit = donation.nonprofit + donation.supporter.save! + end + end + + def self.delete_donation_fully(donation) + if (donation.campaign_gifts.any?) + donation.campaign_gifts.destroy_all + end + + donation.card.destroy + donation.recurring_donation&.destroy + donation.destroy + end + + def self.change_all_donation_to_supporter(d, new_supporter) + d.supporter = new_supporter + d.save! + + d.activities&.each{|i| i.supporter = new_supporter; i.save!} + d.payments&.each{|i| i.supporter = new_supporter; i.save!} + + d.charges&.each{|i| i.supporter = new_supporter; i.save!} + if (d.card) + d.card.charges.any?{|c| !d.charges.include?(c)} + d.card.holder = new_supporter + d.card.save! + end + + if (d.recurring_donation) + d.recurring_donation.supporter = new_supporter + d.recurring_donation.save! + end + end + + def self.create_new_supporter_on_correct_np(nonprofit, old_supporter) + supporter = nonprofit.supporters.build + supporter.name = old_supporter.name + supporter.email = old_supporter.email + supporter.phone = old_supporter.phone + supporter.organization = old_supporter.organization + supporter.address = old_supporter.address + supporter.city = old_supporter.city + supporter.state_code = old_supporter.state_code + supporter.zip_code = old_supporter.zip_code + supporter.country = old_supporter.country + supporter.deleted = true + supporter.save! + supporter + end +end \ No newline at end of file diff --git a/lib/maintain/maintain_payment_records.rb b/app/legacy_lib/maintain_payment_records.rb similarity index 100% rename from lib/maintain/maintain_payment_records.rb rename to app/legacy_lib/maintain_payment_records.rb diff --git a/app/legacy_lib/maintain_payments_where_supporter_is_gone.rb b/app/legacy_lib/maintain_payments_where_supporter_is_gone.rb new file mode 100644 index 000000000..934a14d18 --- /dev/null +++ b/app/legacy_lib/maintain_payments_where_supporter_is_gone.rb @@ -0,0 +1,109 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module MaintainPaymentsWhereSupporterIsGone + + def self.weird_records() + Payment.find_by_sql('SELECT payments.* from payments + LEFT JOIN supporters ON payments.supporter_id = supporters.id + WHERE payments.supporter_id IS NOT NULL AND supporters.id IS NULL') + end + + def self.records_by_nonprofit_urgency( records ) + records_by_nonprofit_urgency = records.group_by{|i| i.nonprofit_id}.sort_by{|k,v| v.count}.reverse + records_by_nonprofit_urgency.map{|k,v| [k, v.count]}.each{|k,v| puts "#{k}: #{v} records"} + return records_by_nonprofit_urgency + end + + def self.sorted_by_kind( records ) + sorted_by_kind = records.group_by{|i| i.kind}.sort_by{|k,v| v.count}.reverse + sorted_by_kind.map{|k,v| [k, v.count]}.each{|k,v| puts "#{k}: #{v} records"} + return sorted_by_kind + end + + def self.nonprofit_by_kind( urgency ) + nonprofit_by_kind = urgency.map{|k,v| [k, v.group_by{|i|i.kind}.sort_by{|i,x| x.count}.reverse.map{|i,x| [i, x.count]}]} + nonprofit_by_kind.each{|id,group| puts id; group.each{|kind, num| puts " #{kind}: #{num}"}} + nonprofit_by_kind + end + + + + + + + def self.cleanup(sorted_by_kind, api_key) + Qx.transaction do + manual_payments = [] + + recurring_donations_from_stripe = sorted_by_kind[1][1].select{|i| i.charge && i.charge.stripe_charge_id && !i.charge.stripe_charge_id.start_with?('legacy')} + donations_from_stripe = sorted_by_kind[2][1].select{|i| i.charge && i.charge.stripe_charge_id && !i.charge.stripe_charge_id.start_with?('legacy')} + ticket_from_stripe = sorted_by_kind[3][1].select{|i| i.charge && i.charge.stripe_charge_id && !i.charge.stripe_charge_id.start_with?('legacy')} + + payments = recurring_donations_from_stripe.concat(donations_from_stripe).concat(ticket_from_stripe) + + payments.each do |i| + begin + unless Supporter.exists?(i.supporter_id) || i.nonprofit_id == 4500 + ch = Stripe::Charge.retrieve(i.charge.stripe_charge_id, {api_key: api_key}) + billing_name = ch.billing_details['name'] + cust = Stripe::Customer.retrieve(ch.customer, {api_key: api_key}) + email = cust.email + + #where we save the Supporter + s = Supporter.create(id: i.supporter_id, name: billing_name, email: email, created_at: i.created_at, nonprofit_id: i.nonprofit_id ) + s.save! + puts "#{i.supporter_id} is saved" + else + puts "#{i.supporter_id} was already saved" + end + rescue => e + puts e + + puts "we failed on #{i.id}" + manual_payments.push(i) + + end + end + + manual_refunds = [] #we have to manually track down these refunds on the connected accounts + refunds = sorted_by_kind[4][1].select{|i| i.refund && i.refund.stripe_refund_id} + + refunds.each do |i| + begin + unless Supporter.exists?(i.supporter_id) + refund = Stripe::Refund.retrieve(i.refund.stripe_refund_id, {api_key: api_key}) + billing_name = refund.billing_details['name'] + cust = Stripe::Customer.retrieve(refund.customer, {api_key: api_key}) + email = cust.email + + #where we save the Supporter + Supporter.create(id: i.supporter_id, name: billing_name, email: email, created_at: i.created_at ) + end + rescue + manual_refunds.push(i) + + end + end + + disputes = sorted_by_kind[5][1].select{|i| i.dispute && i.dispute.stripe_dispute_id} + manual_disputes = [] # ditto + + disputes.each do |i| + begin + unless Supporter.exists?(i.supporter_id) + dispute = Stripe::Dispute.retrieve(i.refund.stripe_refund_id, {api_key: api_key}) + billing_name = dispute.billing_details['name'] + cust = Stripe::Customer.retrieve(dispute.customer, {api_key: api_key}) + email = cust.email + + #where we save the Supporter + Supporter.create(id: i.supporter_id, name: billing_name, email: email, created_at: i.created_at ) + end + rescue + manual_disputes.push(i) + + end + end + return {manual_payments: manual_payments, manual_refunds: manual_refunds, manual_disputes: manual_disputes} + end + end +end diff --git a/app/legacy_lib/maintain_stripe_records.rb b/app/legacy_lib/maintain_stripe_records.rb new file mode 100644 index 000000000..55babf1b0 --- /dev/null +++ b/app/legacy_lib/maintain_stripe_records.rb @@ -0,0 +1,12 @@ +module MaintainStripeRecords + + def self.safely_fill_stripe_charge_object(stripe_charge_id) + LockManager.with_transaction_lock(stripe_charge_id) do + unless StripeCharge.where("stripe_charge_id = ?", stripe_charge_id).any? + object = Stripe::Charge.retrieve(stripe_charge_id) + StripeCharge.create!(object:object) + end + end + end + +end \ No newline at end of file diff --git a/lib/maintain/maintain_ticket_records.rb b/app/legacy_lib/maintain_ticket_records.rb similarity index 100% rename from lib/maintain/maintain_ticket_records.rb rename to app/legacy_lib/maintain_ticket_records.rb diff --git a/app/legacy_lib/maintain_ticket_validity.rb b/app/legacy_lib/maintain_ticket_validity.rb new file mode 100644 index 000000000..b9a6b190c --- /dev/null +++ b/app/legacy_lib/maintain_ticket_validity.rb @@ -0,0 +1,181 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module MaintainTicketValidity + + # some tickets have invalid records. Find them. + def self.get_invalid_tickets + tickets = Ticket.includes({:charge => [{:supporter => [:nonprofit]}]}, {:supporter => :nonprofit}, {:payment => [{:supporter => :nonprofit}, :nonprofit]}, {:event => :nonprofit}) + tickets = tickets.map{|t| {ticket:t, issues:[]}} + + invalid = [] + + first_level(tickets) + + a, tickets = tickets.partition{|i| i[:issues].any?} + invalid = invalid.concat a + + second_level(tickets) + a, tickets = tickets.partition{|i| i[:issues].any?} + invalid = invalid.concat(a) + invalid + end + + # some tickets have valid records, format a report of them + def self.report(invalid_records) + output = invalid_records.map{|t| + ticket = t[:ticket] + { + ticket_id: ticket.id, + ticket_supporter_id: ticket.supporter_id, + event_id: ticket.event_id, + event_name: ticket.event&.name, + event_nonprofit_id: ticket.event&.nonprofit_id, + event_nonprofit_name: ticket.event&.nonprofit&.name, + supporter_id: ticket.supporter_id, + supporter_name: ticket.supporter&.name, + supporter_nonprofit_id: ticket.supporter&.nonprofit_id, + supporter_nonprofit_name: ticket.supporter&.nonprofit&.name, + payment_id: ticket.payment_id, + payment_supporter_id: ticket.payment&.supporter_id, + charge_id: ticket.charge_id, + ticket_date: ticket.created_at, + errors: t[:issues] + } + } + end + + def self.has_no_supporter(t) + if !t[:ticket].supporter + t[:issues].push(:no_supporter) + end + end + + def self.has_no_event(t) + if !t[:ticket].event + t[:issues].push(:no_event) + end + end + + def self.first_level(tickets) + tickets.each do |t| + has_no_event(t) + has_no_supporter(t) + end + end + + def self.event_and_supporter_no_match(t) + if t[:ticket].event&.nonprofit != t[:ticket].supporter&.nonprofit + t[:issues].push(:event_and_supporter_nps_dont_match) + end + end + + def self.payment_and_supporter_no_match(t) + if t[:ticket].payment && (t[:ticket].payment&.supporter != t[:ticket].supporter) + t[:issues].push(:payment_and_ticket_supporter_no_match) + end + end + + def self.charge_but_no_payment(t) + if t[:ticket].charge && !t[:ticket].payment + t[:issues].push(:charge_but_no_payment) + end + end + + def self.second_level(tickets) + tickets.each do |t| + event_and_supporter_no_match(t) + payment_and_supporter_no_match(t) + charge_but_no_payment(t) + end + end + + # some tickets have invalid records. Clean them up. + def self.cleanup(invalid_tickets, profile_id) + Qx.transaction do + invalid_tickets.each do |t| + if t[:issues].include?(:no_supporter) && t[:issues].include?(:no_event) + next + end + if t[:issues].include?(:no_supporter) + cleanup_for_no_supporter(t[:ticket]) + end + + if t[:issues].include?(:no_event) + cleanup_for_no_event(t[:ticket], profile_id) + end + + if t[:issues].include?(:event_and_supporter_nps_dont_match) + cleanup_for_event_and_supporter_nps_dont_match(t[:ticket]) + end + end + end + end + + def self.cleanup_for_no_supporter(ticket) + np = ticket.event&.nonprofit + if (np && !Supporter.exists?(ticket.supporter_id)) + supporter = np.supporters.build + supporter.deleted = true + if (ticket.supporter_id) + supporter.id = ticket.supporter_id + end + supporter.save! + + if (!ticket.supporter_id) + ticket.supporter = supporter + ticket.save! + end + end + end + + def self.cleanup_for_no_event(ticket, profile_id) + np = ticket.supporter&.nonprofit + if(np && !(Event.exists?(ticket.event_id))) + event = np.events.build + event.deleted = true + event.name = "Unnamed event #{ticket.event_id || rand(3000)}" + event.start_datetime = ticket.created_at + event.end_datetime = ticket.created_at + 1.hour + event.address = 'unknown' + event.city = "city" + event.state_code = "wi" + event.zip_code = "55555" + event.profile_id = profile_id + event.slug = "unnamed_event__#{rand(4400)}" + if (ticket.event_id) + event.id = ticket.event_id + end + event.save! + end + end + + def self.cleanup_for_event_and_supporter_nps_dont_match(ticket) + np = ticket.event.nonprofit + old_supporter = ticket.supporter + supporter = np.supporters.build + supporter.name = old_supporter.name + supporter.email = old_supporter.email + supporter.phone = old_supporter.phone + supporter.organization = old_supporter.organization + supporter.address = old_supporter.address + supporter.city = old_supporter.city + supporter.state_code = old_supporter.state_code + supporter.zip_code = old_supporter.zip_code + supporter.country = old_supporter.country + supporter.deleted = true + supporter.save! + + ticket.supporter = supporter + ticket.save! + + end + + def self.find_ticket_groups + payments = Ticket.select('payment_id').where('payment_id IS NOT NULL').group('payment_id').map{|i| i.payment_id} + + payments.select do |p| + tickets = Ticket.where('payment_id = ? ', p) + supporter = tickets.first.supporter_id + !tickets.all? {|t| t.supporter_id == supporter} + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/merge_supporters.rb b/app/legacy_lib/merge_supporters.rb new file mode 100644 index 000000000..5394f8892 --- /dev/null +++ b/app/legacy_lib/merge_supporters.rb @@ -0,0 +1,114 @@ +# 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_supporters, new_supporter, np_id, profile_id) + new_supporter_id = new_supporter.id + old_supporter_ids = old_supporters.map{|i| i.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.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 + end + + old_supporters.joins(:cards).each do |supp| + supp.cards.each do |card| + card.holder = new_supporter + card.save! + end + end + + old_supporters = old_supporters.includes(:tag_joins).includes(:custom_field_joins) + old_tags = old_supporters.map{|i| i.tag_joins.map{|j| j.tag_master}}.flatten.uniq + + #delete old tags + InsertTagJoins.in_bulk(np_id, profile_id, old_supporter_ids, + 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}}) + + 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{|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 + }}) + + # 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)) + # Removing any duplicate custom fields UpdateCustomFieldJoins.delete_dupes([new_supporter_id]) + end + + def self.selected(merged_data, supporter_ids,np_id, profile_id, skip_conflicting_custom_fields=false) + old_supporters = Nonprofit.find(np_id).supporters.where('supporters.id IN (?)', supporter_ids) + + if skip_conflicting_custom_fields && conflicting_custom_fields?(old_supporters) + return { json: supporter_ids, status: :failure } + end + + merged_data['anonymous'] = old_supporters.any?{|i| i.anonymous} + new_supporter = Nonprofit.find(np_id).supporters.create!(merged_data) + # Update merged supporters as deleted + Psql.execute(Qexpr.new.update(:supporters, {deleted: true}).where("supporters.id IN ($ids)", ids: supporter_ids)) + # Update all associated tables + self.update_associations(old_supporters, new_supporter, np_id, profile_id) + return {json: new_supporter, status: :ok} + end + + def self.conflicting_custom_fields?(supporters) + cfjs = [] + supporters.each do |supporter| + supporter.custom_field_joins.each do |cfj| + cfjs << { 'custom_field_master_id' => cfj.custom_field_master_id, 'value' => cfj.value } + end + end + + cfjs.group_by{|i| i['custom_field_master_id']}.any?{ |id, group| group.uniq.size > 1 } + end + + + # Merge supporters for a nonprofit based on an array of groups of ids, generated from QuerySupporters.dupes_on_email or dupes_on_names + # @return [Array[Array]] an array of arrays of supporter_ids that have conflicting custom fields between them. + def self.merge_by_id_groups(np_id, arr_of_ids, profile_id, skip_conflicting_custom_fields=false) + supporter_ids_with_conflicting_custom_fields = [] + arr_of_ids.select{|arr| arr.count > 1}.each do |ids| + Qx.transaction do + # 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") + ) + # 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}) + + result = MergeSupporters.selected(data, ids, np_id, profile_id, skip_conflicting_custom_fields) + supporter_ids_with_conflicting_custom_fields << ids if result[:status] == :failure + + # Create supporter.created object event + Supporter.find(ids.first).merged_into&.publish_created + end + end + supporter_ids_with_conflicting_custom_fields + end +end diff --git a/app/legacy_lib/migrate/migrate_cover_fees.rb b/app/legacy_lib/migrate/migrate_cover_fees.rb new file mode 100644 index 000000000..1b5d8d86c --- /dev/null +++ b/app/legacy_lib/migrate/migrate_cover_fees.rb @@ -0,0 +1,31 @@ +module Migrate + class MigrateCoverFees + def self.for_nonprofits + MiscellaneousNpInfo.all.each do |mni| + if (mni.hide_cover_fees) + mni.fee_coverage_option_config = 'none' + else + mni.fee_coverage_option_config = nil + end + mni.save! + end + end + + def self.for_campaigns + MiscCampaignInfo.all.each do |mci| + if (mci.campaign.nonprofit.hide_cover_fees? ) + mci.fee_coverage_option_config = nil + else + if (mci.hide_cover_fees_option?) + mci.fee_coverage_option_config = 'none' + elsif (mci.manual_cover_fees?) + mci.fee_coverage_option_config = 'manual' + else + mci.fee_coverage_option_config = nil + end + end + mci.save! + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/multiple_condition_search.rb b/app/legacy_lib/multiple_condition_search.rb new file mode 100644 index 000000000..5e59852e5 --- /dev/null +++ b/app/legacy_lib/multiple_condition_search.rb @@ -0,0 +1,77 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# This class assists in searches where you know you want to try a search with list of conditions and stop when your query +# returns a single object. +# For example, let's say you want to search for a single Supporter by name and then, if you still have multiple records, then by name and email. +# +# Assumption: the searches start from the least complex and go up. Probably won't work another way. + +# A multiple condition search like that can end in a few different ways: +# * we get to a condition and there are no records +# * we get to the last condition and there are mulitiple records left +# * we get to a condition where there is a single record (success!) + +# @example Search for all supporters for a particular Nonprofit using name and then name and email +# search = MultipleConditionSearch.new([ +# ['name = ?', "Penelope Schultz"], # you can use any of the styles used by `#where` +# {name: "Penelope Schultz", email: 'penelope@schultz.household'} +# ]) +# result = search.find(Nonprofit.find(12356).supporters) # result is nil if there was an error otherwise, we get the result +# +# puts 'There were no records found' if search.error == :none +# puts 'There were multiple records on the last condition' if search.error == :multiple_values +# +# if search.error == :multiple_values +# puts search.result.pluck(:id).join(',') # prints the ids of the records found by the last condition when multiple values were found +# end +# +# if result +# puts result.id +# end +# + +class MultipleConditionSearch + @subconditions = [] + + # @!attribute [r] error the error from getting to either, getting to a condition where no record can be found by the condition or + # we get to the last condition and there are still multiple records left + # @return [Symbol,nil] nil if a single result was found at one point. :none if a condition returned no values, :multiple_values if + # we got to the last condition and there were multiple records + attr_reader :error + + # @!attribute result the result of the last condition attempted in the find. + # @return [nil,ActiveRecord::Base,ActiveRecord::Relation] nil if the last condition attempted returned no records, + # ActiveRecord::Base if the last query attempted had a single record and ActiveRecord::Relation if the last condition + # attempted had multiple records + attr_reader :result + + # Important note: you MUST wrap all of your conditions into an array + # @param [Array[string,Array,Hash]] args the subconditions attempted. Each of these correspond to the values you would pass into + # the method of where. For example, you could + + def initialize(args=[]) + @subconditions = args + @error = nil + @result = nil + end + + # @param [ActiveRecord::Relation] relation something to run these conditions against + # @return [nil,ActiveRecord::Base] the single record returned from one of the conditions, otherwise nil + def find(relation) + @subconditions.each_with_index do |condition, index| + temp_result = relation.where(condition) + if temp_result.none? + @error = :none + return nil + elsif temp_result.count == 1 + @result = temp_result.first + return @result + elsif index == @subconditions.count - 1 && temp_result.count > 1 + @error = :multiple_values + @result = temp_result + return nil + end + end + raise "should never happen" + end +end \ No newline at end of file diff --git a/lib/name_copy_naming_algorithm.rb b/app/legacy_lib/name_copy_naming_algorithm.rb similarity index 86% rename from lib/name_copy_naming_algorithm.rb rename to app/legacy_lib/name_copy_naming_algorithm.rb index 1fba66445..1094b99bd 100644 --- a/lib/name_copy_naming_algorithm.rb +++ b/app/legacy_lib/name_copy_naming_algorithm.rb @@ -35,7 +35,7 @@ def get_already_used_name_entities(base_name) 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') + @klass.method(:where).call('name SIMILAR TO ? AND nonprofit_id = ?', "#{base_name[0..base_name.length-amount_to_strip-1]}_*" + end_name, nonprofit_id).select('name') end end \ No newline at end of file diff --git a/lib/metrics/nonprofit_metrics.rb b/app/legacy_lib/nonprofit_metrics.rb similarity index 98% rename from lib/metrics/nonprofit_metrics.rb rename to app/legacy_lib/nonprofit_metrics.rb index 73aed5497..0f19322f9 100644 --- a/lib/metrics/nonprofit_metrics.rb +++ b/app/legacy_lib/nonprofit_metrics.rb @@ -55,7 +55,8 @@ def self.recent_donations(np_id) "payments.date", "payments.id AS payment_id", "supporters.name AS supporter_name", - "supporters.email AS supporter_email" + "supporters.email AS supporter_email", + "'/nonprofits/#{np_id}/payments?pid=' || payments.id AS payment_url" ) .from(:payments) .join("supporters", "payments.supporter_id=supporters.id") diff --git a/lib/path/nonprofit_path.rb b/app/legacy_lib/nonprofit_path.rb similarity index 100% rename from lib/path/nonprofit_path.rb rename to app/legacy_lib/nonprofit_path.rb diff --git a/lib/errors/not_enough_quantity_error.rb b/app/legacy_lib/not_enough_quantity_error.rb similarity index 100% rename from lib/errors/not_enough_quantity_error.rb rename to app/legacy_lib/not_enough_quantity_error.rb diff --git a/lib/notify/notify_user.rb b/app/legacy_lib/notify_user.rb similarity index 100% rename from lib/notify/notify_user.rb rename to app/legacy_lib/notify_user.rb diff --git a/lib/numeric.rb b/app/legacy_lib/numeric.rb similarity index 100% rename from lib/numeric.rb rename to app/legacy_lib/numeric.rb diff --git a/lib/onboard_accounts.rb b/app/legacy_lib/onboard_accounts.rb similarity index 99% rename from lib/onboard_accounts.rb rename to app/legacy_lib/onboard_accounts.rb index a04a605fd..0f7bc5241 100644 --- a/lib/onboard_accounts.rb +++ b/app/legacy_lib/onboard_accounts.rb @@ -107,7 +107,6 @@ def self.create_org_with_user(params, user=nil) def self.set_nonprofit_defaults(data) data = data.merge({ - verification_status: 'unverified', published: true, vetted: Settings.nonprofits_must_be_vetted ? false : true, statement: data[:name][0..16], diff --git a/app/legacy_lib/pay_recurring_donation.rb b/app/legacy_lib/pay_recurring_donation.rb new file mode 100644 index 000000000..fb4b60b13 --- /dev/null +++ b/app/legacy_lib/pay_recurring_donation.rb @@ -0,0 +1,130 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# require 'insert/insert_donation' +# 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 + # Bulk insert the delayed jobs with a single expression + 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])} + 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 + end + + # run the payrecurring_donation in development so I can make sure we have the expected failures + # def self._____test_do_not_use_pay_all_due_with_stripe + # # Bulk insert the delayed jobs with a single expression + # ids = Psql.execute_vectors( + # QueryRecurringDonations._all_that_are_due + # )[1..-1].flatten + # + # output = ids.map{|id| + # begin + # i = PayRecurringDonation.with_stripe(id) + # result = {is_error:false, value: i} + # rescue => e + # result = {is_error: true, error_type: e.class.to_s, message: e.message, backtrace: e.backtrace} + # end + # + # result + # } + # + # + # + # return output + # end + + # 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, force_run=false) + ParamValidation.new({:rd_id => rd_id}, { + :rd_id => { + :required => true, + :is_integer=> true + } + }) + + rd = RecurringDonation.includes(:misc_recurring_donation_info).where('id = ?', rd_id).first + + unless rd + raise ParamValidation::ValidationError.new("#{rd_id} is not a valid recurring donation", {:key => :rd_id}) + end + + return false if !force_run && !QueryRecurringDonations.is_due?(rd_id) + + donation = Donation.where('id = ?', rd['donation_id']).first + unless donation + raise ParamValidation::ValidationError.new("#{rd['donation_id']} is not a valid donation", {}) + 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, + 'fee_covered' => rd.misc_recurring_donation_info&.fee_covered + })) + 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('*') + ).first + + InlineJob::ModernObjectDonationStripeChargeJob.perform_later(donation: donation, legacy_payment: result['payment']) + + JobQueue.queue(JobTypes::DonationPaymentCreateJob, rd['donation_id'], result['payment']['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('*') + ).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 + Supporter.find(donation['supporter_id']).supporter_notes.create!(content: "This supporter had a payment failure for their recurring donation with ID #{rd_id}", user: User.find(540)) + end + return result + end + + 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('*') + ).first + DonationMailer.delay.donor_failed_recurring_donation(rd['donation_id']) + if notify_nonprofit + DonationMailer.delay.nonprofit_failed_recurring_donation(rd['donation_id']) + end + Supporter.find(donation['supporter_id']).supporter_notes.create!(content: "This supporter had a payment failure for their recurring donation with ID #{rd['id']}", user: User.find(540)) + return recurring_donation + end +end diff --git a/app/legacy_lib/payment_dupes.rb b/app/legacy_lib/payment_dupes.rb new file mode 100644 index 000000000..7c1ba50a5 --- /dev/null +++ b/app/legacy_lib/payment_dupes.rb @@ -0,0 +1,213 @@ +module PaymentDupes + def self.copy_dedication(source, target) + return true if source.donation.dedication.blank? + return true if target.donation.dedication.present? && (source.donation.dedication.blank? || target.donation.dedication == source.donation.dedication) + return false if target.donation.dedication.present? + target.donation.dedication = source.donation.dedication + target.donation.save! + end + + def self.can_copy_dedication?(source, target) + return true if source.donation.dedication.blank? + return true if target.donation.dedication.present? && (source.donation.dedication.blank? || target.donation.dedication == source.donation.dedication) + return false if target.donation.dedication.present? + true + end + + def self.copy_designation(src, target, designations_to_become_comments) + if designations_to_become_comments.include?(src.donation.designation) + if target.donation&.comment&.include?("Designation: #{src.donation.designation}") + # Already copied, no need to copy again + return true + end + if target.donation.comment.blank? + target.donation.comment = "Designation: " + src.donation.designation + else + target.donation.comment += " \nDesignation: " + src.donation.designation + end + src.donation.designation = nil + target.donation.save! + src.donation.save! + return true + end + return true if src.donation.designation.blank? + return true if target.donation.designation.present? && (src.donation.designation.blank? || target.donation.designation == src.donation.designation) + return false if target.donation.dedication.present? + target.donation.designation = src.donation.designation + target.donation.save! + end + + def self.can_copy_designation?(src, target, designations_to_become_comments) + if designations_to_become_comments.include?(src.donation.designation) + return true + end + return true if src.donation.designation.blank? + return true if target.donation.designation.present? && (src.donation.designation.blank? || target.donation.designation == src.donation.designation) + return false if target.donation.designation.present? + true + end + + def self.copy_comment(source, target, designations_to_become_comments) + return true if source.donation.comment.blank? + return true if target.donation.comment.present? && (source.donation.comment.blank? || target.donation.comment == source.donation.comment) + if target.donation.comment.present? + if designations_to_become_comments.any? { |d| target.donation.comment.include?(d) } + designations_already_copied_to_comment = designations_to_become_comments.select { |d| target.donation.comment.include?(d) } + comment = target.donation.comment + designations_already_copied_to_comment.each do |d| + comment = comment.gsub(" \nDesignation: #{d}", "") + comment = comment.gsub("Designation: #{d}", "") + end + return true if (source.donation.comment.blank? || comment == source.donation.comment) + else + return false + end + end + target.donation.comment = source.donation.comment + target.donation.save! + end + + def self.can_copy_comment?(source, target, designations_to_become_comments) + return true if source.donation.comment.blank? + return true if target.donation.comment.present? && (source.donation.comment.blank? || target.donation.comment == source.donation.comment) + if target.donation.comment.present? + if designations_to_become_comments.any? { |d| target.donation.comment.include?(d) } + designations_already_copied_to_comment = designations_to_become_comments.select { |d| target.donation.comment.include?(d) } + comment = target.donation.comment + designations_already_copied_to_comment.each do |d| + comment = comment.gsub(" \nDesignation: #{d}", "") + comment = comment.gsub("Designation: #{d}", "") + end + return (source.donation.comment.blank? || comment == source.donation.comment) + else + return false + end + end + true + end + + def self.remove_payment_dupes(np_id, designations_to_become_comments) + deleted_payments = [] + nonprofit = Nonprofit.find(np_id) + etap_id_cf = CustomFieldMaster.find_by(name: 'E-Tapestry Id #').id + supp = nonprofit.supporters.not_deleted.joins(:custom_field_joins).where( + 'custom_field_joins.custom_field_master_id = ?', etap_id_cf + ).references(:custom_field_joins) + + supp.find_each do |s| + offsite_payments = s.payments.includes(:donation).where("kind = 'OffsitePayment'").joins(:journal_entries_to_item) + offsite_payments.find_each do |offsite| + # match one offsite donation with an online donation if: + # - the offsite donation was created on the same day that we ran the import and + # - the offsite donation has the same date as the online payment + # - there is a journal entry item for the offsite payment + donation_or_ticket_payments = s.payments.not_matched.includes(:donation).joins( + 'LEFT JOIN nonprofits ON payments.nonprofit_id = nonprofits.id' + ).where( + "(kind = 'Donation' OR kind = 'Ticket' OR kind = 'RecurringDonation') + AND (gross_amount = ? OR net_amount = ?) AND + (to_char(timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', date)), 'YYYY-MM-DD') = ? OR to_char(date, 'YYYY-MM-DD') = ?)", offsite.gross_amount, offsite.gross_amount, offsite.date.strftime('%Y-%m-%d'), offsite.date.strftime('%Y-%m-%d')) + donation_or_ticket_payments.find_each do |online| + reasons = [] + ActiveRecord::Base.transaction do + if online.kind == 'Ticket' + Activity.where(attachment_id: offsite.id, attachment_type: 'Payment').destroy_all + offsite&.offsite_payment&.destroy + offsite.destroy + deleted_payments << offsite.id + if online.payment_dupe_status.present? + online.payment_dupe_status.matched = true + online.payment_dupe_status.matched_with_offline << offsite.id + online.payment_dupe_status.save! + else + online.payment_dupe_status = PaymentDupeStatus.create!(matched: true, matched_with_offline: [offsite.id]) + end + elsif offsite.donation.event.present? && offsite.donation.event != online.donation.event + # different events, dont delete + elsif offsite.donation.campaign.present? && offsite.donation.campaign != online.donation.campaign + # different campaigns, dont delete + else + unless can_copy_comment?(offsite, online, designations_to_become_comments) + reasons << 'Comment' + end + unless can_copy_dedication?(offsite, online) + reasons << 'Dedication' + end + unless can_copy_designation?(offsite, online, designations_to_become_comments) + reasons << 'Designation' + end + if reasons.none? + if online.kind == 'RecurringDonation' + # addresses all the payments from that recurring donation so we avoid future problems + recurring_donation = online.donation + recurring_payments = recurring_donation.payments + temp_duplicate_payments = [] + temp_offsite_matches = [] + recurring_payments.find_each do |recurring_payment| + equivalent_offsite = s.payments.not_matched.where( + "kind = 'OffsitePayment' AND (gross_amount = ? OR gross_amount = ?) AND (to_char(payments.date, 'YYYY-MM-DD') = ? OR to_char(payments.date, 'YYYY-MM-DD') = ?)", + recurring_payment.gross_amount, recurring_payment.net_amount, recurring_payment.date.in_time_zone(nonprofit.timezone).strftime('%Y-%m-%d'), recurring_payment.date.strftime('%Y-%m-%d') + ).joins(:journal_entries_to_item) + if equivalent_offsite.count == 1 + # match! + temp_offsite_matches << equivalent_offsite.first.id + temp_duplicate_payments << equivalent_offsite.first.id.to_s + if recurring_payment.payment_dupe_status.present? + recurring_payment.payment_dupe_status.matched = true + recurring_payment.payment_dupe_status.matched_with_offline << equivalent_offsite.first.id + recurring_payment.payment_dupe_status.save! + else + recurring_payment.payment_dupe_status = PaymentDupeStatus.create!(matched: true, matched_with_offline: [equivalent_offsite.first.id]) + end + if equivalent_offsite.first.payment_dupe_status.present? + equivalent_offsite.first.payment_dupe_status.matched = true + equivalent_offsite.first.payment_dupe_status.matched_with_offline << equivalent_offsite.first.id + equivalent_offsite.first.payment_dupe_status.save! + else + equivalent_offsite.first.payment_dupe_status = PaymentDupeStatus.create!(matched: true, matched_with_offline: [equivalent_offsite.first.id]) + end + end + end + if temp_offsite_matches.any? + # it's the same donation for all of them so + # we can do the copies once + copy_comment(offsite, online, designations_to_become_comments) + copy_dedication(offsite, online) + copy_designation(offsite, online, designations_to_become_comments) + deleted_payments.concat(temp_duplicate_payments) + # deletes matching offsites here + temp_duplicate_payments.each do |op| + op = Payment.find(op) + Activity.where(attachment_id: op.id, attachment_type: 'Payment').destroy_all + op&.offsite_payment&.destroy + op&.donation&.destroy + op&.destroy + end + else + raise ActiveRecord::Rollback + end + else + copy_comment(offsite, online, designations_to_become_comments) + copy_dedication(offsite, online) + copy_designation(offsite, online, designations_to_become_comments) + Activity.where(attachment_id: offsite.id, attachment_type: 'Payment').destroy_all + offsite.donation.destroy + offsite&.offsite_payment&.destroy + offsite.destroy + deleted_payments << offsite.id.to_s + if online.payment_dupe_status.present? + online.payment_dupe_status.matched = true + online.payment_dupe_status.matched_with_offline << offsite.id + online.payment_dupe_status.save! + else + online.payment_dupe_status = PaymentDupeStatus.create!(matched: true, matched_with_offline: [offsite.id]) + end + end + end + end + end + end + end + end + end +end diff --git a/app/legacy_lib/periodic_report_adapter.rb b/app/legacy_lib/periodic_report_adapter.rb new file mode 100644 index 000000000..d8664d71c --- /dev/null +++ b/app/legacy_lib/periodic_report_adapter.rb @@ -0,0 +1,23 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReportAdapter + extend ActiveSupport::Autoload + + autoload :CancelledRecurringDonationsReport + autoload :FailedRecurringDonationsReport + autoload :ActiveRecurringDonationsToCsvReport + autoload :StartedRecurringDonationsToCsvReport + + + REPORT = 'Report' + private_constant :REPORT + + class << self + def build(options) + lookup(options[:report_type]).new(**options) + end + + def lookup(type) + const_get(type.to_s.camelize << REPORT) + end + end +end diff --git a/app/legacy_lib/periodic_report_adapter/active_recurring_donations_to_csv_report.rb b/app/legacy_lib/periodic_report_adapter/active_recurring_donations_to_csv_report.rb new file mode 100644 index 000000000..245fa1f2e --- /dev/null +++ b/app/legacy_lib/periodic_report_adapter/active_recurring_donations_to_csv_report.rb @@ -0,0 +1,23 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReportAdapter::ActiveRecurringDonationsToCsvReport < PeriodicReportAdapter + def initialize(options) + @nonprofit_id = options[:nonprofit_id] + @users = options[:users] + @nonprofit_s3_key = options[:nonprofit_s3_key] + @filename = options[:filename] + end + + def run + ActiveRecurringDonationsToCsvJob.perform_later params + end + + private + + def params + { nonprofit: nonprofit, nonprofit_s3_key: @nonprofit_s3_key, user: @users.first, filename: @filename} + end + + def nonprofit + Nonprofit.find(@nonprofit_id) + end +end \ No newline at end of file diff --git a/app/legacy_lib/periodic_report_adapter/cancelled_recurring_donations_report.rb b/app/legacy_lib/periodic_report_adapter/cancelled_recurring_donations_report.rb new file mode 100644 index 000000000..8bf16c088 --- /dev/null +++ b/app/legacy_lib/periodic_report_adapter/cancelled_recurring_donations_report.rb @@ -0,0 +1,29 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReportAdapter::CancelledRecurringDonationsReport < PeriodicReportAdapter + def initialize(options) + @nonprofit_id = options[:nonprofit_id] + @period = options[:period] + @user_ids = options[:users].pluck(:id) + end + + def run + ExportRecurringDonations::initiate_export(@nonprofit_id, params, @user_ids, :cancelled_recurring_donations_automatic_report) + end + + private + + def params + { :active => false }.merge(period) + end + + def period + method(@period.to_sym).call + end + + def last_month + { + :cancelled_at_gt_or_eq => (Time.current - 1.month).beginning_of_month, + :cancelled_at_lt => Time.current.beginning_of_month + } + end +end diff --git a/app/legacy_lib/periodic_report_adapter/failed_recurring_donations_report.rb b/app/legacy_lib/periodic_report_adapter/failed_recurring_donations_report.rb new file mode 100644 index 000000000..46bd3a21b --- /dev/null +++ b/app/legacy_lib/periodic_report_adapter/failed_recurring_donations_report.rb @@ -0,0 +1,29 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReportAdapter::FailedRecurringDonationsReport < PeriodicReportAdapter + def initialize(options) + @nonprofit_id = options[:nonprofit_id] + @period = options[:period] + @user_ids = options[:users].pluck(:id) + end + + def run + ExportRecurringDonations::initiate_export(@nonprofit_id, params, @user_ids, :failed_recurring_donations_automatic_report) + end + + private + + def params + { :failed => true, :include_last_failed_charge => true }.merge(period) + end + + def period + method(@period.to_sym).call + end + + def last_month + { + :from_date => (Time.current - 1.month).beginning_of_month, + :before_date => Time.current.beginning_of_month + } + end +end diff --git a/app/legacy_lib/periodic_report_adapter/started_recurring_donations_to_csv_report.rb b/app/legacy_lib/periodic_report_adapter/started_recurring_donations_to_csv_report.rb new file mode 100644 index 000000000..132d0d658 --- /dev/null +++ b/app/legacy_lib/periodic_report_adapter/started_recurring_donations_to_csv_report.rb @@ -0,0 +1,24 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReportAdapter::StartedRecurringDonationsToCsvReport < PeriodicReportAdapter + def initialize(options) + @nonprofit_id = options[:nonprofit_id] + @period = options[:period] + @users = options[:users] + @nonprofit_s3_key = options[:nonprofit_s3_key] + @filename = options[:filename] + end + + def run + StartedRecurringDonationsToCsvJob.perform_later params + end + + private + + def nonprofit + Nonprofit.find(@nonprofit_id) + end + + def params + { nonprofit: nonprofit, nonprofit_s3_key: @nonprofit_s3_key, user: @users.first, filename: @filename } + end +end diff --git a/lib/psql.rb b/app/legacy_lib/psql.rb similarity index 100% rename from lib/psql.rb rename to app/legacy_lib/psql.rb diff --git a/lib/qexpr.rb b/app/legacy_lib/qexpr.rb similarity index 91% rename from lib/qexpr.rb rename to app/legacy_lib/qexpr.rb index f66b14ee7..083ce2ea8 100644 --- a/lib/qexpr.rb +++ b/app/legacy_lib/qexpr.rb @@ -28,17 +28,24 @@ def to_s # Parse an qexpr object into a sql string expression def parse expr = "" + if @tree[:withs]&.any? + expr += "WITH ".bold.light_blue + @tree[:withs].join(",\n") + "\n" + end + if @tree[:insert] expr = "#{@tree[:insert]} #{@tree[:values].blue}" expr += "\nRETURNING ".bold.light_blue + (@tree[:returning] || ['id']).join(', ').blue return expr end - + query_based_expression = @tree[:update] || @tree[:delete_from] || @tree[:select] # Query-based expessions - expr = @tree[:update] || @tree[:delete_from] || @tree[:select] - if expr.nil? || expr.empty? + + if query_based_expression.nil? || query_based_expression.empty? raise ArgumentError.new("Must have a select, update, or delete clause") end + + expr += query_based_expression + 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]}" @@ -140,6 +147,15 @@ def offset(i) Qexpr.new @tree.put(:offset, "\nOFFSET".bold.light_blue + " #{i.to_i}".blue) end + def with(name, expr, materialized:nil) + materialized_text = !materialized.nil? ? (materialized ? "MATERIALIZED" : "NOT MATERIALIZED") : "" + return Qexpr.new( + @tree.put(:withs, + (@tree[:withs] || Hamster::Vector[]).add(name.to_s.blue + " AS #{materialized_text} (\n ".bold.light_blue + " #{expr.is_a?(String) ? expr : expr.parse}".blue + "\n)".bold.light_blue + ) + ) + ) + end def join(table_name, on_expr, data={}) on_expr = Qexpr.interpolate_expr(on_expr, data) @@ -209,7 +225,7 @@ def remove(*keys) # Just uses double-dollar quoting universally. Should be generally safe and easy. # Will return an unquoted value it it's a Fixnum 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" diff --git a/lib/qexpr_query_chunker.rb b/app/legacy_lib/qexpr_query_chunker.rb similarity index 97% rename from lib/qexpr_query_chunker.rb rename to app/legacy_lib/qexpr_query_chunker.rb index c61cf28b0..c64b7cd6a 100644 --- a/lib/qexpr_query_chunker.rb +++ b/app/legacy_lib/qexpr_query_chunker.rb @@ -39,7 +39,7 @@ def self.get_chunk_of_query(offset=nil, limit=nil, skip_header=false, &block) # @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=15000, &block) Enumerator.new do |y| last_export_length = 0 limit = chunk_limit diff --git a/lib/query/query_activities.rb b/app/legacy_lib/query_activities.rb similarity index 100% rename from lib/query/query_activities.rb rename to app/legacy_lib/query_activities.rb diff --git a/lib/query/query_campaign_gifts.rb b/app/legacy_lib/query_campaign_gifts.rb similarity index 100% rename from lib/query/query_campaign_gifts.rb rename to app/legacy_lib/query_campaign_gifts.rb diff --git a/lib/query/query_campaign_metrics.rb b/app/legacy_lib/query_campaign_metrics.rb similarity index 85% rename from lib/query/query_campaign_metrics.rb rename to app/legacy_lib/query_campaign_metrics.rb index b2cc15294..69c58f423 100644 --- a/lib/query/query_campaign_metrics.rb +++ b/app/legacy_lib/query_campaign_metrics.rb @@ -23,7 +23,9 @@ def self.on_donations(campaign_id) 'total_raised'=> result['total_raised'], 'goal_amount'=> campaign.goal_amount, 'show_total_count'=> campaign.show_total_count, - 'show_total_raised'=> campaign.show_total_raised + 'show_total_raised'=> campaign.show_total_raised, + 'starting_point' => campaign.starting_point, + 'goal_is_in_supporters' => campaign.goal_is_in_supporters } end end diff --git a/lib/query/query_campaigns.rb b/app/legacy_lib/query_campaigns.rb similarity index 100% rename from lib/query/query_campaigns.rb rename to app/legacy_lib/query_campaigns.rb diff --git a/lib/query/query_charges.rb b/app/legacy_lib/query_charges.rb similarity index 100% rename from lib/query/query_charges.rb rename to app/legacy_lib/query_charges.rb diff --git a/lib/query/query_custom_fields.rb b/app/legacy_lib/query_custom_fields.rb similarity index 100% rename from lib/query/query_custom_fields.rb rename to app/legacy_lib/query_custom_fields.rb diff --git a/lib/query/query_donations.rb b/app/legacy_lib/query_donations.rb similarity index 99% rename from lib/query/query_donations.rb rename to app/legacy_lib/query_donations.rb index 5c4a43179..5bac8221d 100644 --- a/lib/query/query_donations.rb +++ b/app/legacy_lib/query_donations.rb @@ -1,5 +1,4 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'query/query_supporters' module QueryDonations diff --git a/lib/query/query_email_settings.rb b/app/legacy_lib/query_email_settings.rb similarity index 100% rename from lib/query/query_email_settings.rb rename to app/legacy_lib/query_email_settings.rb diff --git a/lib/query/query_event_discounts.rb b/app/legacy_lib/query_event_discounts.rb similarity index 100% rename from lib/query/query_event_discounts.rb rename to app/legacy_lib/query_event_discounts.rb diff --git a/lib/query/query_event_metrics.rb b/app/legacy_lib/query_event_metrics.rb similarity index 100% rename from lib/query/query_event_metrics.rb rename to app/legacy_lib/query_event_metrics.rb diff --git a/lib/query/query_event_organizer.rb b/app/legacy_lib/query_event_organizer.rb similarity index 100% rename from lib/query/query_event_organizer.rb rename to app/legacy_lib/query_event_organizer.rb diff --git a/lib/query/query_full_contact_infos.rb b/app/legacy_lib/query_full_contact_infos.rb similarity index 96% rename from lib/query/query_full_contact_infos.rb rename to app/legacy_lib/query_full_contact_infos.rb index dc2882469..1d3a71ee9 100644 --- a/lib/query/query_full_contact_infos.rb +++ b/app/legacy_lib/query_full_contact_infos.rb @@ -1,6 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'psql' -require 'qexpr' + module QueryFullContactInfos diff --git a/app/legacy_lib/query_nonprofit_keys.rb b/app/legacy_lib/query_nonprofit_keys.rb new file mode 100644 index 000000000..d751f9aeb --- /dev/null +++ b/app/legacy_lib/query_nonprofit_keys.rb @@ -0,0 +1,12 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + + +module QueryNonprofitKeys + + def self.get_key(npo_id, key_name) + item = Nonprofit.find(npo_id).nonprofit_key&.send(key_name.to_sym) + raise ActiveRecord::RecordNotFound.new("Nonprofit key does not exist: #{key_name}") unless item + item + end + +end diff --git a/app/legacy_lib/query_nonprofits.rb b/app/legacy_lib/query_nonprofits.rb new file mode 100644 index 000000000..83b150b40 --- /dev/null +++ b/app/legacy_lib/query_nonprofits.rb @@ -0,0 +1,126 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module QueryNonprofits + + # def self.all_that_need_payouts + # Psql.execute_vectors( + # Qexpr.new.select( + # "nonprofits.id", + # "nonprofits.stripe_account_id", + # "'support@commitchange.com' AS email", + # "'192.168.0.1' AS user_ip", + # "bank_accounts.name" + # ).from(:nonprofits) + # .join(:stripe_accounts, "stripe_accounts.stripe_account_id= nonprofits.stripe_account_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" + # ) + # )[1..-1].select{|i| i.stripe_account&.verification_status == :verified} + # end + + def self.by_search_string(string) + results = Psql.execute_vectors( + Qexpr.new.select( + "nonprofits.id", + "nonprofits.name" + ).from(:nonprofits) + .where("lower(nonprofits.name) LIKE lower($search)", search: "%#{string}%") + .where("nonprofits.published='t'") + .order_by("nonprofits.name ASC") + .limit(10) + )[1..-1] + if results + results = results.map {|id, name| {id: id, name: name}} + end + return results + end + + def self.for_admin(params) + expr = Qx.select( + 'nonprofits.id', + 'nonprofits.name', + 'nonprofits.email', + 'nonprofits.state_code', + 'nonprofits.created_at::date::text AS created_at', + 'nonprofits.vetted', + 'nonprofits.houid', + 'nonprofits.stripe_account_id', + '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(:stripe_accounts, "nonprofits.stripe_account_id=stripe_accounts.stripe_account_id") + .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( + nonprofits.name ILIKE $search + OR nonprofits.email ILIKE $search + OR nonprofits.city ILIKE $search + OR nonprofits.id = $id + ), search: '%' + params[:search] + '%', id: params[:search].to_i) + end + + results = expr.execute + results.map do |i| + np = Nonprofit.includes(:stripe_account).find(i["id"]) + if np.stripe_account + i['verification_status'] = np.stripe_account.verification_status + else + i['verification_status'] = :unverified + end + i + end + end + + 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 + 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)} + end +end diff --git a/app/legacy_lib/query_payments.rb b/app/legacy_lib/query_payments.rb new file mode 100644 index 000000000..62348b58e --- /dev/null +++ b/app/legacy_lib/query_payments.rb @@ -0,0 +1,504 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module QueryPayments + + + def self.get_nonprofit_query(npo_id) + Qexpr.new.select("*").from(:nonprofits).where("id = $id", id: npo_id).limit(1) + end + + def self.get_nonprofit_payments_query(npo_id) + Qexpr.new.select("*").from(:payments).where("nonprofit_id = $id", id: npo_id) + end + + # 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 + # * If the CRD is a refund, then it has a corresponding payment_id and the disbursed is NULL OR the disbursed is marked as false 'f' + # * If the CRD is a charge, the status is set to available + # * If the CRD is a dispute, the status is set to lost ('lost' means the money was refunded to the customer) + # + # In all cases (I think), a corresponding payment for a CRD should exist with the appropriate change in the nonprofit's balance included. This means: + # * For charges, gross_amount should be positive since we're increasing the nonprofit's balance + # * 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={}) + 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(:dispute_transactions, 'dispute_transactions.payment_id=payments.id') + .add_left_join(:manual_balance_adjustments, "manual_balance_adjustments.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 dispute_transactions.payment_id IS NOT NULL OR manual_balance_adjustments.payment_id IS NOT NULL") + .and_where(%Q( + ((refunds.payment_id IS NOT NULL AND refunds.disbursed IS NULL) OR refunds.disbursed='f') + OR (charges.status='available') + OR (dispute_transactions.disbursed='f' + OR (NOT manual_balance_adjustments.disbursed)) + )) + .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? + 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') + .from(:payments) + .where("payments.id IN ($ids)", ids: payment_ids) + .execute.first + end + + + + + def self.nonprofit_balances(npo_id) + payout_totals = QueryPayments.get_payout_totals(QueryPayments.ids_for_payout(npo_id)) + + pending = Nonprofit.find(npo_id).payments.pending_totals + { + 'available' => {'net' => payout_totals['net_amount'] || 0, 'gross' => payout_totals['gross_amount'] || 0}, + 'pending' => {'net' => pending['net'] || 0, 'gross' => pending['gross'] || 0} + } + end + + + def self.full_search(npo_id, query) + limit = 30 + offset = Qexpr.page_offset(limit, query[:page]) + expr = full_search_expr(npo_id, query).select( + 'payments.kind', + 'payments.towards', + 'payments.id AS id', + 'supporters.name', + 'supporters.email', + 'payments.gross_amount', + 'timezone( + COALESCE(nonprofits.timezone, \'UTC\'), + timezone(\'UTC\', payments.date) + ) as date' + ) + + 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') + + 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') + .inner_join('supporters', "supporters.id=payments.supporter_id") + .inner_join('nonprofits', 'nonprofits.id=payments.nonprofit_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] + expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") + end + if ['asc', 'desc'].include? query[:sort_date] + expr = expr.order_by("payments.date #{query[:sort_date]}") + end + if ['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] + expr = expr.order_by("payments.kind #{query[:sort_type]}") + end + if ['asc', 'desc'].include? query[:sort_towards] + expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") + end + + expr = expr.with(:nonprofits, get_nonprofit_query(npo_id), materialized: true) + expr = expr.with(:payments, get_nonprofit_payments_query(npo_id), materialized: false) + + return 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?) + inner_donation_search = inner_donation_search.where('donations.event_id=$id', id: query[:event_id]) + end + 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 + + # We are including deleted supporters on this query because deleted supporters may have made + # payments. + expr = Qexpr.new.select('payments.id').from('payments') + .inner_join('supporters', "supporters.id=payments.supporter_id") + .inner_join('nonprofits', 'nonprofits.id=payments.nonprofit_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[:ids].present? + if query[:ids].is_a? String + query[:ids] = query[:ids].split(',') + end + if query[:ids].is_a?(Array) && query[:ids].all?{|i| i.to_i != 0} + expr = expr.where("payments.id IN ($ids)", ids: query[:ids]) + end + end + if query[:search].present? + expr = SearchVector.query(query[:search], expr) + end + unless (query[:campaign_id].present? || query[:event_id].present?) # if we need to add the reverse query, we can't add this here. + if ['asc', 'desc'].include? query[:sort_amount] + expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") + end + if ['asc', 'desc'].include? query[:sort_date] + expr = expr.order_by("payments.date #{query[:sort_date]}") + end + if ['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] + expr = expr.order_by("payments.kind #{query[:sort_type]}") + end + if ['asc', 'desc'].include? query[:sort_towards] + expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") + end + end + if query[:after_date].present? + expr = expr.where('payments.date >= timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $date))', date: query[:after_date]) + end + if query[:before_date].present? + expr = expr.where('payments.date <= timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $date))', date: query[:before_date]) + end + if query[:amount_greater_than].present? + expr = expr.where('payments.gross_amount >= $amt', amt: query[:amount_greater_than].to_i * 100) + end + if query[:amount_less_than].present? + expr = expr.where('payments.gross_amount <= $amt', amt: query[:amount_less_than].to_i * 100) + end + if query[:year].present? + expr = + expr + .where( + "to_char(timezone( + COALESCE(nonprofits.timezone, \'UTC\'), + timezone(\'UTC\', payments.date) + ), 'YYYY')=$year", + year: (query[:year]).to_s + ) + end + if query[:designation].present? + expr = expr.where("donations.designation @@ $s", s: "#{query[:designation]}") + end + if query[:dedication].present? + expr = expr.where("donations.dedication @@ $s", s: "#{query[:dedication]}") + end + if query[:donation_type].present? + expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(',')) + end + + if query[:check_number].present? + expr = expr + .join( + "offsite_payments", + "offsite_payments.payment_id = payments.id AND offsite_payments.check_number = $check_number", + check_number: query[:check_number] + ) + end + + 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]) + 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") + 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]) + + end + + if query[:anonymous].present? + expr = if(query[:anonymous] == 'true') + expr.where( + '(supporters.anonymous OR donations.anonymous)' + ) + else + expr.where( + '(NOT supporters.anonymous AND NOT donations.anonymous)' + ) + end + end + + if query[:payout_id].present? + expr = expr.join("payment_payouts", "payment_payouts.payout_id = $payout_id AND payment_payouts.payment_id = payments.id", {payout_id: query[:payout_id].to_i}) + end + + if query.has_key? :supporter_covered_fee + + if query[:supporter_covered_fee] + expr = expr.join("misc_payment_infos", "misc_payment_infos.payment_id = payments.id") + expr = expr.where("COALESCE(misc_payment_infos.fee_covered, false)") + else + expr = expr.left_outer_join("misc_payment_infos", "misc_payment_infos.payment_id = payments.id") + expr = expr.where("misc_payment_infos.id IS NULL OR NOT COALESCE(misc_payment_infos.fee_covered, false)") + end + end + + if query.has_key? :online_payments_only + if query[:online_payments_only] + expr = expr.join(:charges, "charges.payment_id = payments.id AND charges.status != $status", status: 'failed') + end + end + + if query[:tag_master_id] + expr = expr.join(:tag_joins, "tag_joins.tag_master_id = $tag_master_id AND tag_joins.supporter_id = supporters.id", tag_master_id: query[:tag_master_id].to_i) + end + + #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 + 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) + inner_donation_search = Qexpr.new.select('donations.*').from('donations') + 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?) + 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') + .inner_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) + + + if query[:search].present? + expr = SearchVector.query(query[:search], expr) + end + + # This breaks so we need to scrap it + # if ['asc', 'desc'].include? query[:sort_amount] + # expr = expr.order_by("payments.gross_amount #{query[:sort_amount]}") + # end + # if ['asc', 'desc'].include? query[:sort_date] + # expr = expr.order_by("payments.date #{query[:sort_date]}") + # end + # if ['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] + # expr = expr.order_by("payments.kind #{query[:sort_type]}") + # end + # if ['asc', 'desc'].include? query[:sort_towards] + # expr = expr.order_by("NULLIF(payments.towards, '') #{query[:sort_towards]}") + # end + if query[:after_date].present? + expr = expr.where('payments.date >= $date', date: query[:after_date]) + end + if query[:before_date].present? + expr = expr.where('payments.date <= $date', date: query[:before_date]) + end + if query[:amount_greater_than].present? + expr = expr.where('payments.gross_amount >= $amt', amt: query[:amount_greater_than].to_i * 100) + end + if query[:amount_less_than].present? + expr = expr.where('payments.gross_amount <= $amt', amt: query[:amount_less_than].to_i * 100) + end + if query[:year].present? + 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]}") + end + if query[:dedication].present? + expr = expr.where("donations.dedication @@ $s", s: "#{query[:dedication]}") + end + if query[:donation_type].present? + expr = expr.where('payments.kind IN ($kinds)', kinds: query[:donation_type].split(',')) + end + + if query[:check_number].present? + expr = expr + .join( + "offsite_payments", + "offsite_payments.payment_id = payments.id AND offsite_payments.check_number = $check_number", + check_number: query[:check_number] + ) + end + + 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]) + 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") + 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]) + + end + + expr + 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") + 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" + ) + .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) + .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(:anonymous)) + .concat([ + "coalesce(donations.designation, 'None') AS \"Designation\"", + "donations.dedication AS \"Honorarium/Memorium\"", + "(donations.anonymous OR supporters.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(misc_payment_infos.fee_covered, 'f') AS \"Fee Covered?\"" + ]) + ) + .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)") + .add_left_join(:misc_payment_infos, "payments.id=misc_payment_infos.payment_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) + pay = Payment.includes(:donation).includes(:offsite_payment) + if (id) + pay = pay.where('id = ?', id) + end + pay = pay.where('date IS NOT NULL').order('id ASC') + pay.all.each{|p| + next if 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) + 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) + 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 + + 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" + end + + def self.query_payout_info(npo_id, payout_id) + tickets_subquery = Qx.select("payment_id", "MAX(event_id) AS event_id").from("tickets").group_by("payment_id").as("tickets") + 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(:anonymous)) + .concat([ + "coalesce(donations.designation, 'None') AS \"Designation\"", + "donations.dedication AS \"Honorarium/Memorium\"", + "(donations.anonymous OR supporters.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") + 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(',')})" + end +end diff --git a/lib/query/query_profiles.rb b/app/legacy_lib/query_profiles.rb similarity index 98% rename from lib/query/query_profiles.rb rename to app/legacy_lib/query_profiles.rb index cf3b7e564..c219f36b3 100644 --- a/lib/query/query_profiles.rb +++ b/app/legacy_lib/query_profiles.rb @@ -1,5 +1,5 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'qexpr' + module QueryProfiles diff --git a/app/legacy_lib/query_recurring_donations.rb b/app/legacy_lib/query_recurring_donations.rb new file mode 100644 index 000000000..f34ee5e8a --- /dev/null +++ b/app/legacy_lib/query_recurring_donations.rb @@ -0,0 +1,406 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +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") + .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 + + return recurring_donation if !recurring_donation || !recurring_donation['id'] + + supporter = Psql.execute( + Qexpr.new.select('*') + .from('supporters') + .where('id=$id', id: recurring_donation['supporter_id']) + ).first + + 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) + include_last_failed_charge = !!query[:include_last_failed_charge] + 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') + .left_outer_join('misc_recurring_donation_infos', + 'misc_recurring_donation_infos.recurring_donation_id = recurring_donations.id') + .where('recurring_donations.nonprofit_id=$id', id: np_id.to_i) + + if include_last_failed_charge + expr = expr.left_outer_join("(SELECT created_at, donation_id FROM charges WHERE charges.status = 'failed') AS failed_charges", 'failed_charges.donation_id = donations.id') + end + + failed_or_active_clauses = [] + + if query.key?(:active_and_not_failed) + clause = query[:active_and_not_failed] ? is_external_active_clause('recurring_donations') : is_external_cancelled_clause('recurring_donations') + failed_or_active_clauses.push("(#{clause})") + end + + if query.key?(:active) + clause = query[:active] ? [is_active_clause('recurring_donations'), is_not_fulfilled_clause('recurring_donations')].join(" AND ") : [is_cancelled_clause('recurring_donations'), is_not_fulfilled_clause('recurring_donations')].join(" AND ") + failed_or_active_clauses.push("(#{clause})") + end + + if query.key?(:failed) + clause = query[:failed] ? [is_failed_clause('recurring_donations'), is_active_clause('recurring_donations')].join(" AND ") : is_not_failed_clause('recurring_donations') + failed_or_active_clauses.push("(#{clause})") + end + + if query.key?(:fulfilled) + clause = query[:fulfilled] ? [is_fulfilled_clause('recurring_donations'), is_active_clause('recurring_donations')].join(" AND ") : is_not_fulfilled_clause('recurring_donations') + failed_or_active_clauses.push("(#{clause})") + end + + if (failed_or_active_clauses.any?) + expr = expr.where("#{failed_or_active_clauses.join(' OR ')}") + end + + + + if query.key?(:end_date_gt_or_equal) + expr = expr.where("recurring_donations.end_date IS NULL OR recurring_donations.end_date >= $date", date: query[:end_date_gt_or_equal]) + end + + if include_last_failed_charge && query.key?(:from_date) && query.key?(:before_date) + expr = expr.where( + 'failed_charges.created_at >= $from_date + AND failed_charges.created_at < $before_date', + { from_date: query[:from_date], before_date: query[:before_date] } + ) + 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') + if query[:cancelled_at_gt_or_equal].present? + expr = expr.where('recurring_donations.cancelled_at >= $date', date: query[:cancelled_at_gt_or_equal]) + end + + if query[:cancelled_at_lt].present? + expr = expr.where('recurring_donations.cancelled_at < $date', date: query[:cancelled_at_lt]) + end + + if query[:search].present? + matcher = "%#{query[:search].downcase.split(' ').join('%')}%" + expr = expr.where(%Q(( + 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}) + end + return expr + end + + + # Fetch the full table of results for the dashboard + def self.full_list(np_id, query={}) + limit = 30 + offset = Qexpr.page_offset(limit, query[:page]) + expr = full_search_expr(np_id, query).select( + 'recurring_donations.start_date', + 'recurring_donations.interval', + 'recurring_donations.time_unit', + 'recurring_donations.n_failures', + 'recurring_donations.amount', + 'recurring_donations.id AS id', + '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) + + data = Psql.execute(expr) + total_count = Psql.execute( + Qexpr.new.select('COUNT(rds)') + .from(full_search_expr(np_id, query).remove(:order_by).select('recurring_donations.id'), 'rds') + ).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]), + } + end + + def self.for_export_enumerable(npo_id, query, chunk_limit=15000) + 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| + 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 ) + root_url = query[:root_url] || 'https://us.commitchange.com/' + include_stripe_customer_id = query[:include_stripe_customer_id] + include_last_failed_charge = !!query[:include_last_failed_charge] + select_list = ['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"', + 'MAX(donations.designation) AS "Designation"', + 'BOOL_OR(misc_recurring_donation_infos.fee_covered) AS "Fee Covered by Supporter"', + "CASE WHEN #{is_cancelled_clause('recurring_donations')} + THEN 'cancelled' + WHEN #{is_failed_clause('recurring_donations')} + THEN 'failed' + WHEN #{is_fulfilled_clause('recurring_donations')} + THEN 'fulfilled' + ELSE 'active' END AS status", + 'recurring_donations.cancelled_at AS "Cancelled At"', + "CASE WHEN (#{is_active_clause('recurring_donations')} OR #{is_failed_clause('recurring_donations')}) AND #{is_not_fulfilled_clause('recurring_donations')} THEN concat('#{root_url}recurring_donations/', recurring_donations.id, '/edit?t=', recurring_donations.edit_token) ELSE '' END AS \"Donation Management Url\"", + 'MAX(recurring_donations.paydate) AS "Paydate"', + 'MAX(paid_charges.created_at) AS "Last Charge Succeeded"' + ] + + if include_last_failed_charge + select_list.push('MAX(failed_charges.created_at) AS "Last Failed Charge"') + end + + if include_stripe_customer_id + select_list.push 'MAX(cards.stripe_customer_id) AS "Stripe Customer ID"' + end + return QexprQueryChunker.get_chunk_of_query(offset, limit, skip_header) { + full_search_expr(npo_id, query).select(select_list) + .left_outer_join('campaigns' , 'campaigns.id=donations.campaign_id') + .left_outer_join('cards', 'cards.id=donations.card_id') + # .left_outer_join('misc_recurring_donation_infos', 'recurring_donations.id = misc_recurring_donation_infos.recurring_donation_id') + } + 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") + 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() + 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) + ).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) + .where("(recurring_donation_holds.id IS NULL OR recurring_donation_holds.end_date IS NULL OR (recurring_donation_holds.end_date IS NOT NULL AND recurring_donation_holds.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" + ) + .left_outer_join('recurring_donation_holds', 'recurring_donation_holds.recurring_donation_id = recurring_donations.id') + .where(%Q( + last_charge.donation_id IS NULL + OR ( + (recurring_donations.time_unit != 'month' OR recurring_donations.interval != 1) + AND last_charge.created_at + concat_ws(' ', recurring_donations.interval, recurring_donations.time_unit)::interval <= $now + ) + OR ( + recurring_donations.time_unit='month' AND recurring_donations.interval=1 + AND (last_charge.created_at < $beginning_of_last_month) + AND (recurring_donation_holds.end_date IS NULL OR recurring_donation_holds.end_date < $beginning_of_last_month) + OR ( + recurring_donations.time_unit='month' AND recurring_donations.interval=1 + AND (last_charge.created_at < $beginning_of_month) + AND ( + recurring_donations.paydate IS NOT NULL + AND recurring_donations.paydate <= $today + OR + recurring_donations.paydate IS NULL + AND extract(day FROM last_charge.created_at) <= $today + ) + ) + ) + ), { + 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( + 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" + ) + .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) + ).first + end + + # External active means what a user would consider active, i.e. a recurring donation that will be paid. + # This means it hasn't be cancelled "active='t'" and that it hasn't failed 'n_failures < 3' + def self.is_external_active_clause(field_for_rd) + "#{is_active_clause(field_for_rd)} AND #{is_not_failed_clause(field_for_rd)} AND #{is_not_fulfilled_clause(field_for_rd)}" + end + + def self.is_external_cancelled_clause(field_for_rd) + "(#{is_cancelled_clause(field_for_rd)}) AND #{is_not_failed_clause(field_for_rd)} AND (#{is_not_fulfilled_clause(field_for_rd)})" + end + + def self.is_active_clause(field_for_rd) + "#{field_for_rd}.active='t'" + end + + def self.is_fulfilled_clause(field_for_rd) + "#{field_for_rd}.end_date IS NOT NULL AND #{field_for_rd}.end_date < '#{Time.current}'" + end + + def self.is_not_fulfilled_clause(field_for_rd) + "NOT(#{is_fulfilled_clause(field_for_rd)})" + end + + + def self.is_cancelled_clause(field_for_rd) + "NOT (#{is_active_clause(field_for_rd)})" + end + + def self.is_not_failed_clause(field_for_rd) + "coalesce(#{field_for_rd}.n_failures, 0) < 3" + end + + def self.is_failed_clause(field_for_rd) + "coalesce(#{field_for_rd}.n_failures, 0) >= 3" + 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") + 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 + end + + def self.get_active_recurring_for_an_org(nonprofit) + supporter_groups_by_email = nonprofit.supporters.joins(:recurring_donations).where("recurring_donations.active AND recurring_donations.n_failures < 3").references(:recurring_donations).group("supporters.email").select("supporters.email, ARRAY_AGG(supporters.id) AS supporters") + + result = generate_output_by_supporter_email_groups(supporter_groups_by_email) + + + Format::Csv.from_data(result, titleize_header: false) + + + + end + + def self.get_new_recurring_for_an_org_during_a_period(nonprofit, start_date_for_search=nil , end_date_for_search=nil) + if start_date_for_search.nil? || end_date_for_search.nil? + start_date_for_search = Time.current.beginning_of_month - 1.month + end_date_for_search = Time.current.beginning_of_month + end + supporter_groups_by_email = nonprofit.supporters.joins(:recurring_donations) + .where("recurring_donations.active AND recurring_donations.n_failures < 3 and recurring_donations.start_date >= ? and recurring_donations.start_date < ?", start_date_for_search, end_date_for_search).references(:recurring_donations).group("supporters.email").select("supporters.email, ARRAY_AGG(supporters.id) AS supporters") + + result = generate_output_by_supporter_email_groups(supporter_groups_by_email) + Format::Csv.from_data(result, titleize_header: false) + end + + def self.generate_output_by_supporter_email_groups(supporter_groups_by_email) + supporter_groups_by_email.map do |supporter_group| + all_supporters = supporter_group.supporters.map{|id| Supporter.includes(:recurring_donations, :tag_joins => :tag_master).find(id)} + tags = all_supporters.map{|s| s.tag_joins.joins(:tag_master).where("NOT tag_masters.deleted").references(:tag_masters).pluck("tag_masters.name")}.flatten + recurrings = all_supporters.map{|supporter| supporter.recurring_donations.active.unfailed.order("start_date DESC")}.flatten.sort_by{|i| i.start_date}.reverse + + { + email: supporter_group.email, + tags: tags.join(','), + active_recurring_donations: recurrings.map{|recurring| recurring.amount.to_s + ',' + recurring.start_date.to_datetime.utc.to_i.to_s}.join(';') + } + end + end +end diff --git a/lib/query/query_roles.rb b/app/legacy_lib/query_roles.rb similarity index 100% rename from lib/query/query_roles.rb rename to app/legacy_lib/query_roles.rb diff --git a/lib/query/query_source_token.rb b/app/legacy_lib/query_source_token.rb similarity index 100% rename from lib/query/query_source_token.rb rename to app/legacy_lib/query_source_token.rb diff --git a/lib/query/query_supporters.rb b/app/legacy_lib/query_supporters.rb similarity index 75% rename from lib/query/query_supporters.rb rename to app/legacy_lib/query_supporters.rb index 017a9948b..0d7290cad 100644 --- a/lib/query/query_supporters.rb +++ b/app/legacy_lib/query_supporters.rb @@ -91,7 +91,13 @@ def self.full_search(np_id, query) 'supporters.is_unsubscribed_from_emails', 'supporters.id AS id', 'tags.names AS tags', - "to_char(payments.max_date, 'MM/DD/YY') AS last_contribution", + "to_char( + timezone( + COALESCE(nonprofits.timezone, 'UTC'), + timezone('UTC', payments.max_date) + ), + 'MM/DD/YY' + ) AS last_contribution", 'payments.sum AS total_raised' ] if query[:select] @@ -161,7 +167,7 @@ def self.full_filter_expr(np_id, query) 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) + .from(Qx.select('*').from(:payments).where("nonprofit_id = $id", id: np_id).as(:payments).parse) .join(Qx.select('id') .from(:supporters) .where("supporters.nonprofit_id = $id and deleted != 'true'", id: np_id ) @@ -175,11 +181,13 @@ def self.full_filter_expr(np_id, query) 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") + .where("tag_masters.nonprofit_id = $id AND NOT tag_masters.deleted", id: np_id.to_i) .group_by("tag_joins.supporter_id") .as(:tags) - expr = Qx.select('supporters.id').from(:supporters) + expr = Qx.select('supporters.id') + .from(:supporters) + .join('nonprofits', 'nonprofits.id=supporters.nonprofit_id') .where( ["supporters.nonprofit_id=$id", id: np_id.to_i], ["supporters.deleted != true"] @@ -191,41 +199,46 @@ def self.full_filter_expr(np_id, query) .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 > timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $d))", d: Chronic.parse(query[:last_payment_after]).beginning_of_day) 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 < timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $d))", d: Chronic.parse(query[:last_payment_before]).beginning_of_day) 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 > timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $d))", d: Chronic.parse(query[:first_payment_after]).beginning_of_day) 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 < timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $d))", d: Chronic.parse(query[:first_payment_before]).beginning_of_day) end - if query[:total_raised_greater_than].present? - expr = expr.and_where("payments.sum > $amount", amount: query[:total_raised_greater_than].to_i * 100) + if query[:total_raised_greater_than_or_equal].present? + expr = expr.and_where("payments.sum >= $amount", amount: query[:total_raised_greater_than_or_equal].to_s.gsub(/[^\d.]/, '').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_s.gsub(/[^\d.]/, '').to_i * 100) end if ['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 >= timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $d))", d: d) end if ['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 <= timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $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 < timezone(COALESCE(nonprofits.timezone, \'UTC\'), timezone(\'UTC\', $date)) OR payments.count = 0", date: date_ago) end if query[:search].present? expr = expr.and_where(%Q( - supporters.name ILIKE $search - OR supporters.email ILIKE $search - OR supporters.organization ILIKE $search - ), search: '%' + query[:search] + '%') + supporters.fts @@ websearch_to_tsquery('english', $search) + OR ( + supporters.phone IS NOT NULL + AND supporters.phone != '' + AND supporters.phone_index IS NOT NULL + AND supporters.phone_index != '' + AND supporters.phone_index = (regexp_replace($search, '\\D','', 'g')) + ) + ), search: query[:search], old_search: '%' + query[:search] + '%') end if query[:notes].present? notes_subquery = Qx.select("STRING_AGG(content, ' ') as content, supporter_id") @@ -233,7 +246,7 @@ def self.full_filter_expr(np_id, query) .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]) + .and_where("to_tsvector('english', notes.content) @@ websearch_to_tsquery('english', $notes)", notes: query[:notes]) end if query[:custom_fields].present? c_f_subquery = Qx.select("STRING_AGG(value, ' ') as value", "supporter_id") @@ -241,11 +254,16 @@ def self.full_filter_expr(np_id, query) .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]) + .and_where("to_tsvector('english', custom_fields.value) @@ websearch_to_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) end + + if query[:anonymous].present? + exprt = expr.and_where('COALESCE(supporters.anonymous, false)') + end + if query[:recurring].present? rec_ps_subquery = Qx.select("payments.count", "payments.supporter_id") .from(:payments) @@ -267,9 +285,13 @@ def self.full_filter_expr(np_id, query) 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 - .get_campaign_and_children(query[:campaign_id].to_i) - .parse})") + expr = expr + .add_join( + Qx.select("donations.supporter_id").from(:donations).where("donations.campaign_id IN (#{QueryCampaigns + .get_campaign_and_children(query[:campaign_id].to_i) + .parse})").group_by(:supporter_id).as(:donations), + "donations.supporter_id= supporters.id" + ) end if query[:event_id].present? @@ -313,7 +335,7 @@ def self.full_filter_expr(np_id, query) return expr end - def self.for_export_enumerable(npo_id, query, chunk_limit=35000) + def self.for_export_enumerable(npo_id, query, chunk_limit=15000) ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, query: {required:true, is_hash: true}}) @@ -379,7 +401,7 @@ def self.get_chunk_of_export(np_id, query, offset=nil, limit=nil, skip_header=fa end end - def self.supporter_note_export_enumerable(npo_id, query, chunk_limit=35000) + def self.supporter_note_export_enumerable(npo_id, query, chunk_limit=15000) ParamValidation.new({npo_id: npo_id, query:query}, {npo_id: {required: true, is_int: true}, query: {required:true, is_hash: true}}) @@ -447,10 +469,11 @@ def self.for_export(np_id, query) expr.select(selects).execute(format: 'csv') end - def self.supporter_export_selections - [ + def self.supporter_export_selections(*remove) + result = [ "substring(trim(both from supporters.name) from '^.+ ([^\s]+)$') AS \"Last Name\"", "substring(trim(both from supporters.name) from '^(.+) [^\s]+$') AS \"First Name\"", + "substring(trim(both from supporters.name) from '^([^\s]+).*$') AS \"Just First Name\"", "trim(both from supporters.name) AS \"Full Name\"", "supporters.organization AS \"Organization\"", "supporters.email \"Email\"", @@ -460,9 +483,12 @@ def self.supporter_export_selections "supporters.state_code \"State\"", "supporters.zip_code \"Postal Code\"", "supporters.country \"Country\"", - "supporters.anonymous \"Anonymous?\"", "supporters.id \"Supporter ID\"" ] + if (!remove.include? :anonymous) + result = result.push("supporters.anonymous \"Anonymous?\"") + end + result end # Return an array of groups of ids, where sub-array is a group of duplicates @@ -472,7 +498,7 @@ def self.dupes_expr(np_id) Qx.select("ARRAY_AGG(id) AS ids") .from(:supporters) .where("nonprofit_id=$id", id: np_id) - .and_where("deleted='f' OR deleted IS NULL") + .and_where("NOT deleted") .having('COUNT(id) > 1') end @@ -481,32 +507,162 @@ def self.dupes_expr(np_id) # Find all duplicate supporters by the email column # returns array of arrays of ids # (each sub-array is a group of duplicates) - def self.dupes_on_email(np_id) + def self.dupes_on_email(np_id, strict_mode = true) + group_by_clause = strict_mode ? strict_email_match : loose_email_match dupes_expr(np_id) .and_where("email IS NOT NULL") .and_where("email != ''") - .group_by(:email) + .group_by(group_by_clause) .execute(format: 'csv')[1..-1] - .map(&:flatten) + .map { |arr_group| arr_group.flatten.sort } end # Find all duplicate supporters by the name column - def self.dupes_on_name(np_id) + def self.dupes_on_name(np_id, strict_mode = true) + group_by_clause = strict_mode ? strict_name_match : loose_name_match dupes_expr(np_id) .and_where("name IS NOT NULL") - .group_by(:name) + .group_by(group_by_clause) .execute(format: 'csv')[1..-1] - .map(&:flatten) + .map { |arr_group| arr_group.flatten.sort } end # Find all duplicate supporters that match on both name/email # @return [Array[Array]] an array containing arrays of the ids of duplicate supporters - def self.dupes_on_name_and_email(np_id) + def self.dupes_on_name_and_email(np_id, strict_mode = true) + group_by_clause = (strict_mode ? [strict_name_match, 'email'] : [loose_name_match, loose_email_match]).join(', ') + dupes_expr(np_id) + .and_where("name IS NOT NULL AND name != '' AND email IS NOT NULL AND email != ''") + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_name_and_phone(np_id, strict_mode = true) + group_by_clause = [(strict_mode ? strict_name_match : loose_name_match), 'phone_index'].join(', ') dupes_expr(np_id) - .and_where("name IS NOT NULL AND email IS NOT NULL AND email != ''") - .group_by("name, email") + .and_where( + "name IS NOT NULL\ + AND name != ''\ + AND phone_index IS NOT NULL \ + AND phone_index != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_name_and_phone_and_address(np_id, strict_mode = true) + group_by_clause = (strict_mode ? [strict_name_match, strict_address_match] : [loose_name_match, loose_address_match]).append("phone_index").join(', ') + dupes_expr(np_id) + .and_where( + "name IS NOT NULL\ + AND name != ''\ + AND phone_index IS NOT NULL \ + AND phone_index != '' \ + AND address IS NOT NULL \ + AND address != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_phone_and_email_and_address(np_id, strict_mode = true) + group_by_clause = (strict_mode ? [strict_address_match, strict_email_match] : [loose_address_match, loose_email_match]).append("phone_index").join(', ') + dupes_expr(np_id) + .and_where( + "phone_index IS NOT NULL \ + AND phone_index != '' \ + AND email IS NOT NULL\ + AND email != ''\ + AND address IS NOT NULL \ + AND address != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_address(np_id, strict_mode = true) + group_by_clause = strict_mode ? strict_address_match : loose_address_match + dupes_expr(np_id) + .and_where( + "address IS NOT NULL \ + AND address != ''" + ) + .group_by(group_by_clause) .execute(format: 'csv')[1..-1] - .map(&:flatten) + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_name_and_address(np_id, strict_mode = true) + group_by_clause = (strict_mode ? [strict_name_match, strict_address_match] : [loose_name_match, loose_address_match]).join(', ') + dupes_expr(np_id) + .and_where( + "name IS NOT NULL\ + AND name != ''\ + AND address IS NOT NULL \ + AND address != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_phone_and_email(np_id, strict_mode = true) + group_by_clause = [(strict_mode ? strict_email_match : loose_email_match), "phone_index"].join(', ') + dupes_expr(np_id) + .and_where( + "phone_index IS NOT NULL \ + AND phone_index != '' \ + AND email IS NOT NULL\ + AND email != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.dupes_on_address_without_zip_code(np_id, strict_mode = true) + group_by_clause = strict_mode ? "btrim(lower(address), ' ')" : "regexp_replace (lower(address),'[^0-9a-z]','','g')" + dupes_expr(np_id) + .and_where( + "address IS NOT NULL \ + AND address != ''" + ) + .group_by(group_by_clause) + .execute(format: 'csv')[1..-1] + .map { |arr_group| arr_group.flatten.sort } + end + + def self.strict_address_match + "btrim(lower(address), ' '), substring(zip_code from '(([0-9]+.*)*[0-9]+)')" + end + + def self.strict_name_match + "btrim(lower(name), ' ')" + end + + def self.strict_email_match + "email" + end + + def self.loose_email_match + "btrim(lower(email), ' ')" + end + + def self.loose_address_match + loose_address_match_chunks.join(", ") + end + + def self.loose_address_match_chunks + ["regexp_replace (lower(address),'[^0-9a-z]','','g')", + "substring(zip_code from '(([0-9]+.*)*[0-9]+)')"] + end + + def self.loose_name_match + "regexp_replace (lower(name),'[^0-9a-z]','','g')" end # Create an export that lists donors with their total contributed amounts @@ -514,7 +670,7 @@ def self.dupes_on_name_and_email(np_id) # 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"]) ) + .select( supporter_export_selections.concat(["(payments.sum::numeric / 100.0)::money::text AS \"Total Contributions #{year}\"", "supporters.id"]) ) .from(:supporters) .join(Qexpr.new .select("SUM(gross_amount)", "supporter_id") @@ -530,7 +686,7 @@ def self.end_of_year_donor_report(np_id, year) Qexpr.new .select( "supporters.*", - '(payments.gross_amount / 100.0)::money::text AS "Donation Amount"', + '(payments.gross_amount::numeric / 100.0)::money::text AS "Donation Amount"', 'payments.date AS "Donation Date"', 'payments.towards AS "Designation"' ) @@ -613,7 +769,7 @@ def self.merge_data(ids) Qx.select(*QuerySupporters.profile_selects) .from("supporters") .group_by("supporters.id") - .where("supporters.id IN ($ids)", ids: ids.split(',')) + .where("supporters.id IN ($ids)", ids: ids) .execute end @@ -633,7 +789,7 @@ def self.year_aggregate_report(npo_id, time_range_params) array_to_string( array_agg( payments.date::date || ' ' || - (payments.gross_amount / 100)::text::money || ' ' || + (payments.gross_amount::numeric / 100)::text::money || ' ' || coalesce(payments.kind, '') || ' ' || coalesce(payments.towards, '') ORDER BY payments.date DESC @@ -642,9 +798,9 @@ def self.year_aggregate_report(npo_id, time_range_params) ) AS "Payment History" ) selects = supporter_export_selections.concat([ - "SUM(payments.gross_amount / 100)::text::money AS \"Total Payments\"", + "SUM(payments.gross_amount::numeric / 100)::text::money AS \"Total Payments\"", "MAX(payments.date)::date AS \"Last Payment Date\"", - "AVG(payments.gross_amount / 100)::text::money AS \"Average Payment\"", + "AVG(payments.gross_amount::numeric / 100)::text::money AS \"Average Payment\"", aggregate_dons ]) return Qx.select(selects) diff --git a/lib/query/query_ticket_levels.rb b/app/legacy_lib/query_ticket_levels.rb similarity index 80% rename from lib/query/query_ticket_levels.rb rename to app/legacy_lib/query_ticket_levels.rb index 6e2fea0e9..b20dd5ae6 100644 --- a/lib/query/query_ticket_levels.rb +++ b/app/legacy_lib/query_ticket_levels.rb @@ -3,22 +3,17 @@ 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 if discount_id perc = EventDiscount.find(discount_id).percent - total = total - (total * (perc / 100.0)).round + total = BigDecimal(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") diff --git a/lib/query/query_tickets.rb b/app/legacy_lib/query_tickets.rb similarity index 90% rename from lib/query/query_tickets.rb rename to app/legacy_lib/query_tickets.rb index f85828931..d28ec839f 100644 --- a/lib/query/query_tickets.rb +++ b/app/legacy_lib/query_tickets.rb @@ -68,13 +68,14 @@ def self.attendees_expr(event_id, query) def self.attendees_list(event_id, query) + nonprofit = Event.find(event_id).nonprofit limit = 30 offset = Qexpr.page_offset(limit, query[:page]) data = Psql.execute( attendees_expr(event_id, query) .limit(limit).offset(offset) - .select(*attendees_list_selection) + .select(*attendees_list_selection(nonprofit)) ) total_count = Psql.execute( @@ -99,7 +100,7 @@ def self.attendees_list(event_id, query) def self.for_export(event_id, query) - Psql.execute_vectors( + data = Psql.execute_vectors( attendees_expr(event_id, query) .select([ "tickets.bid_id AS id", @@ -110,13 +111,25 @@ def self.for_export(event_id, query) "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?\"" + "tickets.source_token_id" ].concat(QuerySupporters.supporter_export_selections)) ) + + data[0].push('Card Saved?') + data.drop(1).each{|i| + source_token_id = i[8] + if (source_token_id && QuerySourceToken.source_token_unexpired?(SourceToken.find(source_token_id))) + i.push("Yes") + else + i.push("") + end + } + + data end - def self.attendees_list_selection + def self.attendees_list_selection(nonprofit) ['tickets.id', 'tickets.bid_id', 'tickets.checked_in', @@ -131,6 +144,7 @@ def self.attendees_list_selection 'supporters.id AS supporter_id', 'supporters.name AS name', 'supporters.email AS email', + "'/nonprofits/#{nonprofit.id}/supporters?sid=' || supporters.id AS supporter_url", 'coalesce(donations.total_amount, 0) AS total_donations', 'source_tokens.token AS token', 'cards.name AS card_name' diff --git a/lib/query/query_users.rb b/app/legacy_lib/query_users.rb similarity index 96% rename from lib/query/query_users.rb rename to app/legacy_lib/query_users.rb index 94bcb3c27..3b6688a31 100644 --- a/lib/query/query_users.rb +++ b/app/legacy_lib/query_users.rb @@ -1,7 +1,4 @@ # 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 diff --git a/lib/qx_query_chunker.rb b/app/legacy_lib/qx_query_chunker.rb similarity index 100% rename from lib/qx_query_chunker.rb rename to app/legacy_lib/qx_query_chunker.rb diff --git a/app/legacy_lib/reassign_supporter_items.rb b/app/legacy_lib/reassign_supporter_items.rb new file mode 100644 index 000000000..b15965977 --- /dev/null +++ b/app/legacy_lib/reassign_supporter_items.rb @@ -0,0 +1,77 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +module ReassignSupporterItems + def self.perform(etap_import) + badly_assigned_items = find_badly_assigned_items(etap_import) + badly_assigned_items.each do |items| + ActiveRecord::Base.transaction do + reassign_journal_entries(items[:supp_through_contact], items[:journal_entries_to_items_with_wrong_supporter], etap_import) + end + end + remaining_badly_assigned_items = find_badly_assigned_items(etap_import) + end + + def self.find_badly_assigned_items(etap_import) + etap_import.e_tap_import_journal_entries.find_each.map do |etije| + supp_through_contact = etije.supporter_through_e_tap_import_contact + if supp_through_contact.blank? + supp_through_contact = ETapImportContact.find_by_account_name(etije.journal_entries_to_items.first.item.supporter.name, etije.journal_entries_to_items.first.item.supporter.email, etije.account_id)&.supporter + end + if etije.journal_entries_to_items.select{|i| i.item.supporter != supp_through_contact}.any? + { + etije: etije, + etije_id: etije.id, + supp_through_contact: supp_through_contact, + journal_entries_to_items_with_wrong_supporter: etije.journal_entries_to_items.select{|i| i.item.supporter != supp_through_contact} + } + else + nil + end + end.compact + end + + def self.reassign_journal_entries(correct_supporter, journal_entries_to_items_with_wrong_supporter, etap_import) + return if correct_supporter.blank? + + journal_entries_to_items_with_wrong_supporter.each do |journal_entry| + reassign_item(correct_supporter, journal_entry.item, etap_import) + end + end + + def self.reassign_item(correct_supporter, item, etap_import) + activities = find_activities(item) + etap_import.reassignments.create(item: item, source_supporter: item.supporter, target_supporter: correct_supporter) + item.supporter = correct_supporter + item.save! + activities.each do |activity| + etap_import.reassignments.create(item: activity, source_supporter: activity.supporter, target_supporter: correct_supporter) + activity.supporter = correct_supporter + activity.save! + end + end + + def self.find_activities(item) + attachment_type = item.class.name + if item.is_a?(SupporterNote) + attachment_type = 'SupporterEmail' + elsif item.is_a?(Donation) || item.is_a?(RecurringDonation) || item.is_a?(Refund) || item.is_a?(OffsitePayment) + attachment_type = 'Payment' + end + Activity.where(attachment_type: attachment_type, attachment_id: item.id) + end + + def self.revert_reassignments_from_supporter(supporter) + reassignments = Reassignment.where(target_supporter: supporter) + reassignments.each do |reassignment| + reassign_item(reassignment.source_supporter, reassignment.item, reassignment.e_tap_import) + end + reassignments.destroy_all + end + + def self.revert_all_reassignments(etap_import) + reassignments = Reassignment.where(e_tap_import: etap_import) + reassignments.each do |reassignment| + reassign_item(reassignment.source_supporter, reassignment.item, reassignment.e_tap_import) + end + reassignments.destroy_all + end +end diff --git a/lib/retrieve/retrieve_active_record_items.rb b/app/legacy_lib/retrieve_active_record_items.rb similarity index 100% rename from lib/retrieve/retrieve_active_record_items.rb rename to app/legacy_lib/retrieve_active_record_items.rb diff --git a/app/legacy_lib/scheduled_jobs.rb b/app/legacy_lib/scheduled_jobs.rb new file mode 100644 index 000000000..6c7e0a5d6 --- /dev/null +++ b/app/legacy_lib/scheduled_jobs.rb @@ -0,0 +1,132 @@ +# 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 + # and wrap each call in begin/rescue/end blocks + # and it can continue to execute all the parts of the job without bailing early, even if one part of the job fails + # And it will aggregate success/failure messages from all the lambdas in the enum + + # Clear out all junk tables. Warning, some of this is dangerous yo! + 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=''") + # 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") + }) + # 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") + }) + + return Enumerator.new do |yielder| + yielder << lambda do + del_cfjs_noval.execute + "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" + end + yielder << lambda do + del_tags_orphaned.execute + "Successfully cleaned up tags that have been orphaned from supporters" + end + end + end + + + def self.pay_recurring_donations + return 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')}" + end + end + end + + def self.update_verification_statuses + # return Enumerator.new do |yielder| + # Nonprofit.where(verification_status: 'pending').each do |np| + # yielder << lambda do + # acct = Stripe::Account.retrieve(np.stripe_account_id) + # verified = acct.payouts_enabled && acct.verification.fields_needed.count == 0 + # np.verification_status = verified ? 'verified' : np.verification_status + # NonprofitMailer.failed_verification_notice(np).deliver if np.verification_status != 'verified' + # NonprofitMailer.successful_verification_notice(np).deliver if np.verification_status == 'verified' + # np.save + # "Status updated for NP #{np.id} as '#{np.verification_status}'" + # end + # end + # end + end + + def self.update_np_balances + return 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) + "Updated charge statuses for NP #{np.id}" + end + end + end + end + + def self.update_pending_payouts + return Enumerator.new do |yielder| + Payout.pending.includes(:nonprofit).each do |p| + yielder << lambda do + err = false + if p.transfer_type == :transfer + p.status = Stripe::Transfer.retrieve(p.stripe_transfer_id, { + stripe_account: p.nonprofit.stripe_account_id + }).status + elsif p.transfer_type == :payout + p.status = Stripe::Payout.retrieve(p.stripe_transfer_id, { + stripe_account: p.nonprofit.stripe_account_id + }).status + end + + p.save + "Updated status for NP #{p.nonprofit.id}, payout # #{p.id}" + end + end + end + end + + def self.delete_expired_source_tokens + return Enumerator.new do |yielder| + yielder << lambda do + tokens_deleted = SourceToken.where("expiration > ?", DateTime.now - 1.day).delete_all + "Deleted #{tokens_deleted} source tokens" + end + end + end + + def self.send_monthly_reports + return Enumerator.new do |yielder| + yielder << lambda do + if Time.current.day == 1 + active_periodic_reports = PeriodicReport.active + active_periodic_reports.each do |report| + report.run + end + "Sent #{active_periodic_reports.count} periodic reports!" + end + end + end + end +end diff --git a/app/legacy_lib/search_vector.rb b/app/legacy_lib/search_vector.rb new file mode 100644 index 000000000..98033f848 --- /dev/null +++ b/app/legacy_lib/search_vector.rb @@ -0,0 +1,36 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module SearchVector + + AcceptedTables = ['supporters', 'payments'] + + def self.query(query_string, expr=nil) + query = if (query_string.is_a?(Integer) || query_string.is_int?) && SearchVector.within_postgres_integer_limit(query_string) + "(supporters.fts @@ websearch_to_tsquery('english', $search::varchar(255)) + OR donations.fts @@ websearch_to_tsquery('english', $search::varchar(255)) + OR ( + supporters.phone IS NOT NULL + AND supporters.phone != '' + AND supporters.phone_index = $search::varchar(255) + ) + OR payments.id = $search::INTEGER)" + else + "(supporters.fts @@ websearch_to_tsquery('english', $search) + OR ( + supporters.phone IS NOT NULL + AND supporters.phone != '' + AND supporters.phone_index IS NOT NULL + AND supporters.phone_index != '' + AND supporters.phone_index = (regexp_replace($search, '\\D','', 'g')) + ) + OR donations.fts @@ websearch_to_tsquery('english', $search))" + end + + (expr || Qexpr.new).where(query, { search: query_string }) + end + + + def self.within_postgres_integer_limit(test_int) + test_int.to_i > 0 && test_int.to_i <= 2147483647 + end +end diff --git a/lib/slug_copy_naming_algorithm.rb b/app/legacy_lib/slug_copy_naming_algorithm.rb similarity index 85% rename from lib/slug_copy_naming_algorithm.rb rename to app/legacy_lib/slug_copy_naming_algorithm.rb index c12bde986..0407c5ec6 100644 --- a/lib/slug_copy_naming_algorithm.rb +++ b/app/legacy_lib/slug_copy_naming_algorithm.rb @@ -22,7 +22,7 @@ def get_name_for_entity(name_entity) def get_already_used_name_entities(base_name) 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') + @klass.method(:where).call('slug SIMILAR TO ? AND nonprofit_id = ?', base_name + end_name, nonprofit_id).select('slug') end end \ No newline at end of file diff --git a/lib/slug_nonprofit_naming_algorithm.rb b/app/legacy_lib/slug_nonprofit_naming_algorithm.rb similarity index 100% rename from lib/slug_nonprofit_naming_algorithm.rb rename to app/legacy_lib/slug_nonprofit_naming_algorithm.rb diff --git a/lib/slug_p2p_campaign_naming_algorithm.rb b/app/legacy_lib/slug_p2p_campaign_naming_algorithm.rb similarity index 100% rename from lib/slug_p2p_campaign_naming_algorithm.rb rename to app/legacy_lib/slug_p2p_campaign_naming_algorithm.rb diff --git a/app/legacy_lib/stripe_account_utils.rb b/app/legacy_lib/stripe_account_utils.rb new file mode 100644 index 000000000..61b93168b --- /dev/null +++ b/app/legacy_lib/stripe_account_utils.rb @@ -0,0 +1,68 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module StripeAccountUtils + + # 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 + + if !np['stripe_account_id'] + return create(np) + else + return np['stripe_account_id'] + end + end + + # np should be a hash with string keys + def self.create(np) + ParamValidation.new({:np => np}, {:np => {:required=> true, :is_a => Nonprofit}}) + params = { + type: 'custom', + email: np['email'].present? ? np['email'] : np.roles.nonprofit_admins.order('created_at ASC').first.user.email, + business_type: 'company', + company: { + name: np['name'], + address: { + city: np['city'], + state: np['state_code'], + postal_code: np['zip_code'], + country: 'US' + } + }, + settings: { + payouts: { + schedule: { + interval: 'manual' + } + } + }, + requested_capabilities: [ + 'card_payments', + 'transfers' + ], + business_profile: { + product_description: 'Nonprofit donations' + } + } + + if np['website'] && np['website'] =~ URI::regexp + params[:business_profile][:url] = np['website'] + end + begin + acct = Stripe::Account.create(params, {stripe_version: '2019-09-09' }) + #byebug + rescue Stripe::InvalidRequestError => e + #byebug + [:state, :postal_code].each {|i| params[:company][:address].delete(i)} + params[:business_profile].delete(:url) + acct = Stripe::Account.create(params, {stripe_version: '2019-09-09' }) + end + Qx.update(:nonprofits).set(stripe_account_id: acct.id).where(id: np['id']).execute + return acct.id + end +end diff --git a/app/legacy_lib/stripe_utils.rb b/app/legacy_lib/stripe_utils.rb new file mode 100644 index 000000000..30c1c8175 --- /dev/null +++ b/app/legacy_lib/stripe_utils.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +require 'stripe' + +module StripeUtils + + def self.create_transfer(net_amount, stripe_account_id, currency) + Stripe::Payout.create({ + amount: net_amount, + currency: currency || Settings.intntl.currencies[0] + }, { + stripe_account: stripe_account_id + }) + end +end diff --git a/app/legacy_lib/supporter_interpolation_dictionary.rb b/app/legacy_lib/supporter_interpolation_dictionary.rb new file mode 100644 index 000000000..3e2c848ff --- /dev/null +++ b/app/legacy_lib/supporter_interpolation_dictionary.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +# InterpolationDictionary is a simple class for replacing braced variables, +# like {{NAME}}, with a different value. We use this for email templates. +class SupporterInterpolationDictionary < InterpolationDictionary + def set_supporter(supporter) + if supporter.is_a?(Supporter) && supporter&.name&.present? + add_entry('NAME', supporter&.name&.strip) + if supporter.name&.strip.split(' ')[0].present? + add_entry('FIRSTNAME', supporter.name&.strip.split(' ')[0]) + end + end + end +end \ No newline at end of file diff --git a/app/legacy_lib/temp_block_error.rb b/app/legacy_lib/temp_block_error.rb new file mode 100644 index 000000000..5c45d6d2b --- /dev/null +++ b/app/legacy_lib/temp_block_error.rb @@ -0,0 +1,3 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class TempBlockError < CCOrgError +end \ No newline at end of file diff --git a/lib/timespan.rb b/app/legacy_lib/timespan.rb similarity index 87% rename from lib/timespan.rb rename to app/legacy_lib/timespan.rb index 305d030eb..ec11d838e 100644 --- a/lib/timespan.rb +++ b/app/legacy_lib/timespan.rb @@ -4,8 +4,8 @@ Timespan = Struct.new(:interval, :time_unit) do - Units = ['week', 'day', 'month', 'year'] - TimeUnits = { + self::Units = ['week', 'day', 'month', 'year'] + self::TimeUnits = { '1_week' => 1.week.ago, '2_weeks' => 2.weeks.ago, '1_month' => 1.month.ago, @@ -28,7 +28,7 @@ def self.later_than_by?(start_date, end_date, 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) + raise(ArgumentError, "time_unit must be one of: #{self::Units}") unless self::Units.include?(time_unit) return interval.send(time_unit.to_sym) end diff --git a/app/legacy_lib/update_activities.rb b/app/legacy_lib/update_activities.rb new file mode 100644 index 000000000..b76088b4a --- /dev/null +++ b/app/legacy_lib/update_activities.rb @@ -0,0 +1,44 @@ +# 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'] + + Qx.update(:activities) + .set(json_data: {content: note[:content], user_email: user_email}.to_json) + .timestamps + .where(attachment_id: note[:id]) + .execute + + end + + def self.for_one_time_donation(payment) + activity = generate_for_one_time_donation(payment) + activity.save! if activity + end + + def self.generate_for_one_time_donation(payment) + donation = payment.donation + activity = payment.activities.first + if activity + activity.date = payment.date + json_data = { + 'gross_amount': payment.gross_amount, + 'designation': donation.designation, + 'dedication': donation.dedication + } + + activity.json_data = json_data + return activity + end + return nil + end +end + diff --git a/lib/update/update_campaign_gift_option.rb b/app/legacy_lib/update_campaign_gift_option.rb similarity index 100% rename from lib/update/update_campaign_gift_option.rb rename to app/legacy_lib/update_campaign_gift_option.rb diff --git a/lib/update/update_charges.rb b/app/legacy_lib/update_charges.rb similarity index 100% rename from lib/update/update_charges.rb rename to app/legacy_lib/update_charges.rb diff --git a/lib/update/update_custom_field_joins.rb b/app/legacy_lib/update_custom_field_joins.rb similarity index 100% rename from lib/update/update_custom_field_joins.rb rename to app/legacy_lib/update_custom_field_joins.rb diff --git a/app/legacy_lib/update_disputes.rb b/app/legacy_lib/update_disputes.rb new file mode 100644 index 000000000..f9b53146f --- /dev/null +++ b/app/legacy_lib/update_disputes.rb @@ -0,0 +1,18 @@ +# 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) + DisputeTransaction.where("payment_id IN (?)", payment_ids).update_all( + disbursed:true, + updated_at:Time.current + ) + end + + def self.reverse_disburse_all_with_payments(payment_ids) + DisputeTransaction.where("payment_id IN (?)", payment_ids).update_all( + disbursed:false, + updated_at:Time.current + ) + end +end diff --git a/lib/update/update_donation.rb b/app/legacy_lib/update_donation.rb similarity index 90% rename from lib/update/update_donation.rb rename to app/legacy_lib/update_donation.rb index ff680b821..98855c6c0 100644 --- a/lib/update/update_donation.rb +++ b/app/legacy_lib/update_donation.rb @@ -71,7 +71,7 @@ def self.update_payment(donation_id, data) end Qx.transaction do - + something_changed = false donation = existing_payment.donation donation.designation = data[:designation] if data[:designation] @@ -98,6 +98,7 @@ def self.update_payment(donation_id, data) existing_payment.net_amount = existing_payment.gross_amount - existing_payment.fee_total if existing_payment.changed? + something_changed = true existing_payment.save! end else @@ -114,15 +115,21 @@ def self.update_payment(donation_id, data) offsite_payment.gross_amount = data[:gross_amount] if data[:gross_amount] if offsite_payment.changed? + something_changed = true offsite_payment.save! end end if donation.changed? + something_changed = true donation.save! end existing_payment.reload + if something_changed + UpdateActivities.for_one_time_donation(existing_payment) + end + ret = donation.attributes ret[:payment] = existing_payment.attributes if is_offsite @@ -132,6 +139,18 @@ def self.update_payment(donation_id, data) end end + # + # Change the dedication on a donation and its payment(s) + # + # @param [Donation] donation + # @param [string] new_dedication The new dedication + # + def self.redesignate_donation(donation, new_designation) + donation.designation = new_designation + donation.payments.each{|i| i.towards = new_designation; i.save!} + donation.save! + end + def self.correct_donations_when_date_and_payments_are_off(id) Qx.transaction do @payments_corrected = [] diff --git a/lib/update/update_email_settings.rb b/app/legacy_lib/update_email_settings.rb similarity index 100% rename from lib/update/update_email_settings.rb rename to app/legacy_lib/update_email_settings.rb diff --git a/app/legacy_lib/update_manual_balance_adjustments.rb b/app/legacy_lib/update_manual_balance_adjustments.rb new file mode 100644 index 000000000..b26930d6b --- /dev/null +++ b/app/legacy_lib/update_manual_balance_adjustments.rb @@ -0,0 +1,11 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module UpdateManualBalanceAdjustments + + def self.disburse_all_with_payments(payment_ids) + ManualBalanceAdjustment.where("payment_id IN (?)", payment_ids).update_all( + disbursed:true, + updated_at:Time.current + ) + end +end diff --git a/lib/update/update_miscellaneous_np_info.rb b/app/legacy_lib/update_miscellaneous_np_info.rb similarity index 100% rename from lib/update/update_miscellaneous_np_info.rb rename to app/legacy_lib/update_miscellaneous_np_info.rb diff --git a/app/legacy_lib/update_nonprofit.rb b/app/legacy_lib/update_nonprofit.rb new file mode 100644 index 000000000..468b3f19c --- /dev/null +++ b/app/legacy_lib/update_nonprofit.rb @@ -0,0 +1,35 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +module UpdateNonprofit + + # 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_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 + + return if pending_payments.empty? + + remaining_balance = stripe_net_balance - cc_net_balance + charge_ids = pending_payments.take_while do |payment| + if payment['net_amount'] <= remaining_balance + remaining_balance -= payment['net_amount'] + true + end + end.map{|h| h['charge_id']} + + 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/app/legacy_lib/update_order.rb similarity index 100% rename from lib/update/update_order.rb rename to app/legacy_lib/update_order.rb diff --git a/lib/update/update_payouts.rb b/app/legacy_lib/update_payouts.rb similarity index 95% rename from lib/update/update_payouts.rb rename to app/legacy_lib/update_payouts.rb index 37a795fa6..3cb8441ff 100644 --- a/lib/update/update_payouts.rb +++ b/app/legacy_lib/update_payouts.rb @@ -26,7 +26,7 @@ def self.reverse_with_stripe(payout_id, status, failure_message) UpdateRefunds.reverse_disburse_all_with_payments(payment_ids) # Mark all disputes as lost_and_paid - #UpdateDisputes.disburse_all_with_payments(payment_ids) + UpdateDisputes.reverse_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 diff --git a/lib/update/update_recurring_donations.rb b/app/legacy_lib/update_recurring_donations.rb similarity index 75% rename from lib/update/update_recurring_donations.rb rename to app/legacy_lib/update_recurring_donations.rb index 7fb21985a..a0c7e47d7 100644 --- a/lib/update/update_recurring_donations.rb +++ b/app/legacy_lib/update_recurring_donations.rb @@ -1,14 +1,17 @@ # 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' -require 'format/date' -require 'format/currency' module UpdateRecurringDonations + class UpdateModel + include ActiveModel::Validations + attr_accessor :amount + validates :amount, :presence => true, :numericality => {:greater_than_or_equal_to => 0.75} + end + # 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}, @@ -38,7 +41,7 @@ def self.update_card_id(rd, token) 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}]) + rec_don.supporter.supporter_notes.create!( content: "This supporter updated their card for their recurring donation with ID #{rec_don.id}", user: User.find(540)) end return QueryRecurringDonations.fetch_for_edit(rd[:id])['recurring_donation'] end @@ -54,7 +57,8 @@ def self.update_paydate(rd, paydate) # @param [RecurringDonation] rd # @param [String] token # @param [Integer] amount - def self.update_amount(rd, token, amount) + # @param [Boolean] fee_covered + def self.update_amount(rd, token, amount, fee_covered=false) ParamValidation.new({amount: amount, rd: rd, token: token}, {amount: {is_integer: true, min: 50, required:true}, rd: {required:true, is_a: RecurringDonation}, @@ -76,9 +80,12 @@ def self.update_amount(rd, token, amount) donation.amount = amount rd.save! donation.save! + misc = rd.misc_recurring_donation_info || rd.create_misc_recurring_donation_info + misc.fee_covered = fee_covered + misc.save! end - EmailJobQueue.queue(JobTypes::NonprofitRecurringDonationChangeAmountJob, rd.id, previous_amount) - EmailJobQueue.queue(JobTypes::DonorRecurringDonationChangeAmountJob,rd.id, previous_amount) + JobQueue.queue(JobTypes::NonprofitRecurringDonationChangeAmountJob, rd.id, previous_amount) + JobQueue.queue(JobTypes::DonorRecurringDonationChangeAmountJob,rd.id, previous_amount) rd end @@ -95,24 +102,29 @@ def self.update_from_end_dates # 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) - Psql.execute( - Qexpr.new.update(:recurring_donations, { - active: false, - cancelled_by: email, - cancelled_at: Time.current - }) - .where("id=$id", id: rd_id.to_i) - ) + recurring_donation = RecurringDonation.find(rd_id) + recurring_donation.cancel!(email) + 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}]) + Supporter.find(rd['supporter_id']).supporter_notes.create!(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: User.find(540)); if (!dont_notify_nonprofit) DonationMailer.delay.nonprofit_recurring_donation_cancellation(rd['donation_id']) end + return rd end def self.update(rd, params) + model = UpdateRecurringDonations::UpdateModel.new + + model.amount = params[:donation] && params[:donation][:dollars] + + model_valid = model.valid? + if !model_valid + return model + end + params = set_defaults(params) if params[:donation] rd.donation.update_attributes(params[:donation]) @@ -120,6 +132,12 @@ def self.update(rd, params) params = params.except(:donation) end + fee_covered = params[:fee_covered] + misc = rd.misc_recurring_donation_info || rd.create_misc_recurring_donation_info + misc.fee_covered = fee_covered + misc.save! + + params = params.except(:fee_covered) rd.update_attributes(params) return rd end diff --git a/lib/update/update_refunds.rb b/app/legacy_lib/update_refunds.rb similarity index 100% rename from lib/update/update_refunds.rb rename to app/legacy_lib/update_refunds.rb diff --git a/lib/update/update_supporter.rb b/app/legacy_lib/update_supporter.rb similarity index 100% rename from lib/update/update_supporter.rb rename to app/legacy_lib/update_supporter.rb diff --git a/lib/update/update_supporter_notes.rb b/app/legacy_lib/update_supporter_notes.rb similarity index 98% rename from lib/update/update_supporter_notes.rb rename to app/legacy_lib/update_supporter_notes.rb index 0c0cdd416..67fbf4e9e 100644 --- a/lib/update/update_supporter_notes.rb +++ b/app/legacy_lib/update_supporter_notes.rb @@ -1,5 +1,4 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -require 'qx' module UpdateSupporterNotes diff --git a/lib/update/update_tickets.rb b/app/legacy_lib/update_tickets.rb similarity index 80% rename from lib/update/update_tickets.rb rename to app/legacy_lib/update_tickets.rb index 393ba2a9a..7f615f190 100644 --- a/lib/update/update_tickets.rb +++ b/app/legacy_lib/update_tickets.rb @@ -96,4 +96,35 @@ def self.validate_entities(entities) raise ParamValidation::ValidationError.new("Ticket ID #{entities[:ticket_id].id} does not belong to event #{entities[:event_id].id}", key: :ticket_id) end end + + + def self.discount_ticket(ticket, discount) + if (ticket.class != Ticket) + ticket = Ticket.find(ticket) + end + + if (discount > 1 || discount < 0 ) + raise ArgumentError.new("Discount must be between 0 and 1. Value was #{discount}") + end + + Qx.transaction do + payment = ticket.payment + payment.gross_amount = payment.gross_amount * (1-discount) + payment.net_amount = payment.net_amount * (1-discount) + payment.save! + + op = payment.offsite_payment + op.gross_amount = op.gross_amount * (1-discount) + op.save! + + activities = ticket.activities.select{|i| i.action_type == 'created'} + activities.each do |a| + data = JSON::parse(a.json_data) + data['gross_amount'] = Integer(data['gross_amount'] * (1-discount)) + a.json_data = JSON::generate(data) + a.save! + end + end + + end end diff --git a/lib/uuid.rb b/app/legacy_lib/uuid.rb similarity index 100% rename from lib/uuid.rb rename to app/legacy_lib/uuid.rb diff --git a/lib/validation_error.rb b/app/legacy_lib/validation_error.rb similarity index 100% rename from lib/validation_error.rb rename to app/legacy_lib/validation_error.rb diff --git a/app/mailers/admin_mailer.rb b/app/mailers/admin_mailer.rb index 11e5250bb..3d98c82b0 100644 --- a/app/mailers/admin_mailer.rb +++ b/app/mailers/admin_mailer.rb @@ -6,9 +6,10 @@ class AdminMailer < BaseMailer # # en.admin_mailer.notify_failed_gift.subject # - def notify_failed_gift(donation, campaign_gift_option) + def notify_failed_gift(donation, payment, campaign_gift_option) @campaign_gift_option = campaign_gift_option @donation = donation + @payment = payment mail subject: "Tried to associate donation #{donation.id} with campaign gift option #{campaign_gift_option.id} which is out of stock", to: Settings.mailer.email, from: Settings.mailer.default_from end end diff --git a/app/mailers/application_mailer.rb b/app/mailers/application_mailer.rb new file mode 100644 index 000000000..d3bddc779 --- /dev/null +++ b/app/mailers/application_mailer.rb @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class ApplicationMailer < BaseMailer + layout 'mailer' +end diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb index 950216474..9817c3967 100644 --- a/app/mailers/base_mailer.rb +++ b/app/mailers/base_mailer.rb @@ -1,8 +1,7 @@ # 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, "X-SES-CONFIGURATION-SET" => 'Admin' layout 'email' end diff --git a/app/mailers/dispute_mailer.rb b/app/mailers/dispute_mailer.rb new file mode 100644 index 000000000..44e9671c9 --- /dev/null +++ b/app/mailers/dispute_mailer.rb @@ -0,0 +1,126 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class DisputeMailer < BaseMailer + include ActionView::Helpers::NumberHelper + + default from: "support@commitchange.com", to: "support@commitchange.com" + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.created.subject + # + + def created(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + + mail subject: t('dispute_mailer.created.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name, + evidence_due_date: @stripe_dispute.evidence_due_date + ) + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.funds_withdrawn.subject + # + def funds_withdrawn(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + @withdrawal_transaction = dispute.dispute_transactions.first + + mail subject: t('dispute_mailer.funds_withdrawn.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name, + amount: print_currency(@withdrawal_transaction.payment.net_amount, "$") , + evidence_due_date: @stripe_dispute.evidence_due_date + ) + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.funds_reinstated.subject + # + def funds_reinstated(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + @reinstated_transaction = dispute.dispute_transactions.second + + mail subject: t('dispute_mailer.funds_reinstated.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name, + amount: print_currency(@reinstated_transaction.payment.net_amount, "$"), + ) + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.closed.subject + # + def won(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + + mail subject: t('dispute_mailer.won.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name + ) + end + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.lost.subject + # + def lost(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + + mail subject: t('dispute_mailer.lost.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name + ) + end + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.dispute_mailer.updated.subject + # + def updated(dispute) + @dispute = dispute + @nonprofit = dispute.nonprofit + @payment = dispute.original_payment + @stripe_dispute = dispute.stripe_dispute + + mail subject: t('dispute_mailer.updated.subject', + dispute_id: @stripe_dispute.stripe_dispute_id, + nonprofit_name: @nonprofit.name, + evidence_due_date: @stripe_dispute.evidence_due_date + ) + + end + + private + + ## from application_helper. I don't have time to mess with this. + 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 +end diff --git a/app/mailers/donation_mailer.rb b/app/mailers/donation_mailer.rb index 35daa7f2d..34b9b9cab 100644 --- a/app/mailers/donation_mailer.rb +++ b/app/mailers/donation_mailer.rb @@ -3,34 +3,41 @@ class DonationMailer < BaseMailer # 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) + def donor_payment_notification(donation_id, payment_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 + @payment = @donation.payments.find(payment_id) + + interpolation_dict.set_supporter(@donation.supporter) + if @donation.campaign && interpolation_dict.interpolate(@donation.campaign.receipt_message).present? + @thank_you_note = interpolation_dict.interpolate(@donation.campaign.receipt_message) else - @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, {'NAME' => @donation.supporter.name}) + @thank_you_note = interpolation_dict.interpolate(@nonprofit.thank_you_note) end - @charge = @donation.charges.last - reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email + @charge = @payment.charge + @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)) + I18n.with_locale(locale) do + unless @donation.supporter.email.blank? + 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 end - def donor_direct_debit_notification(donation_id, locale=I18n.locale) + def donor_direct_debit_notification(donation_id, payment_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 + @nonprofit = @donation.nonprofit + + interpolation_dict.set_supporter(@donation.supporter) + + if @donation.campaign && interpolation_dict.interpolate(@donation.campaign.receipt_message).present? + @thank_you_note = interpolation_dict.interpolate(@donation.campaign.receipt_message) else - @thank_you_note = Format::Interpolate.with_hash(@nonprofit.thank_you_note, {'NAME' => @donation.supporter.name}) + @thank_you_note = interpolation_dict.interpolate(@nonprofit.thank_you_note) end reply_to = @nonprofit.email.blank? ? @nonprofit.users.first.email : @nonprofit.email @@ -46,25 +53,30 @@ def donor_direct_debit_notification(donation_id, locale=I18n.locale) end # Used for both one-time and recurring donations - def nonprofit_payment_notification(donation_id, user_id=nil) + def nonprofit_payment_notification(donation_id, payment_id, user_id=nil) @donation = Donation.find(donation_id) - @charge = @donation.charges.last + @payment = @donation.payments.find(payment_id) + @charge = @payment.charge @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 + if @emails.any? + mail(to: @emails, subject: "Donation receipt for #{@donation.supporter.name}") + end end def nonprofit_failed_recurring_donation(donation_id) @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}") + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, @donation.campaign ? 'notify_campaigns' : 'notify_payments') + if @emails.any? + mail(to: @emails, subject: "Recurring donation payment failure for #{@donation.supporter.name || @donation.supporter.email}") + end end def donor_failed_recurring_donation(donation_id) @@ -80,8 +92,10 @@ def nonprofit_recurring_donation_cancellation(donation_id) @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}") + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, @donation.campaign ? 'notify_campaigns' : 'notify_payments') + if @emails.any? + mail(to: @emails, subject: "Recurring donation cancelled for #{@donation.supporter.name || @donation.supporter.email}") + end end def nonprofit_recurring_donation_change_amount(donation_id, previous_amount=nil) @@ -89,15 +103,20 @@ def nonprofit_recurring_donation_change_amount(donation_id, previous_amount=nil) @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}") + if @emails.any? + mail(to: @emails, subject:"Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") + end 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 + + interpolation_dict.set_supporter(@donation.supporter) + + if @nonprofit.miscellaneous_np_info && interpolation_dict.interpolate(@nonprofit.miscellaneous_np_info.change_amount_message).present? + @thank_you_note = interpolation_dict.interpolate(@nonprofit.miscellaneous_np_info.change_amount_message) else @thank_you_note = nil end @@ -111,21 +130,13 @@ def nonprofit_recurring_donation_change_amount(donation_id, previous_amount=nil) @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}") + if @emails.any? + mail(to: @emails, subject:"Recurring donation amount changed for #{@donation.supporter.name || @donation.supporter.email}") + end 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}") + def interpolation_dict + @interpolation_dict ||= SupporterInterpolationDictionary.new({'NAME' => 'Supporter', 'FIRSTNAME' => 'Supporter'}) end end diff --git a/app/mailers/export_mailer.rb b/app/mailers/export_mailer.rb index 2df3d36fb..70f181d1e 100644 --- a/app/mailers/export_mailer.rb +++ b/app/mailers/export_mailer.rb @@ -52,4 +52,28 @@ def export_supporter_notes_failed_notification(export) @export = export mail(to: @export.user.email, subject: 'Your supporter notes export has failed.') end + + def export_failed_recurring_donations_monthly_completed_notification(export) + @export = export + + mail(to: @export.user.email, subject: "Your report of failed recurring donations from #{Time.now.strftime("%B %Y")} is available!") + end + + def export_failed_recurring_donations_monthly_failed_notification(export) + @export = export + + mail(to: @export.user.email, subject: "Your report of failed recurring donations from #{Time.now.strftime("%B %Y")} has failed.") + end + + def export_cancelled_recurring_donations_monthly_completed_notification(export) + @export = export + + mail(to: @export.user.email, subject: "Your report of cancelled recurring donations from #{Time.now.strftime("%B %Y")} is available!") + end + + def export_cancelled_recurring_donations_monthly_cancelled_notification(export) + @export = export + + mail(to: @export.user.email, subject: "Your report of cancelled recurring donations from #{Time.now.strftime("%B %Y")} has failed.") + end end diff --git a/app/mailers/nonprofit_mailer.rb b/app/mailers/nonprofit_mailer.rb index e57bd62dd..02548e7cb 100644 --- a/app/mailers/nonprofit_mailer.rb +++ b/app/mailers/nonprofit_mailer.rb @@ -1,24 +1,17 @@ # 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') - mail(to: @emails, subject: "We need some further account verification on #{Settings.general.name}") - end - - def successful_verification_notice(np) - @nonprofit = np - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') - mail(to: @emails, subject: "Verification successful on #{Settings.general.name}!") - end - - def refund_notification(refund_id) + def refund_notification(refund_id, user_id=nil) @refund = Refund.find(refund_id) @charge = @refund.charge @nonprofit = @refund.payment.nonprofit - @supporter = @refund.payment.supporter + @supporter = @refund.payment.supporter @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payments') + if user_id + em = User.find(user_id).email + return unless @emails.include?(em) + @emails = [em] + end mail(to: @emails, subject: "A new refund has been made for $#{Format::Currency.cents_to_dollars(@refund.amount)}") end @@ -29,10 +22,10 @@ def new_bank_account_notification(ba) mail(to: @emails, subject: "We need to confirm the new bank account") end - def pending_payout_notification(payout_id) + def pending_payout_notification(payout_id, emails=nil) @payout = Payout.find(payout_id) @nonprofit = @payout.nonprofit - @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + @emails = emails || QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') mail(to: @emails, subject: "Payout of available balance now pending") end @@ -89,21 +82,10 @@ def invoice_payment_notification(nonprofit_id, payment) 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] - from = Format::Name.email_from_np(@nonprofit.name) - mail(to: args[:supporter_email], reply_to: args[:from_email], from: from, subject: args[:subject]) - end - - def setup_verification(np_id) + def first_charge_email(np_id) @nonprofit = Nonprofit.find(np_id) @emails = QueryUsers.all_nonprofit_user_emails(np_id, [:nonprofit_admin]) - mail(to: @emails, reply_to: 'support@commitchange.com', from: "#{Settings.general.name} Support", subject: "Set up automatic payouts on #{Settings.general.name}") + mail(to: @emails, reply_to: 'support@commitchange.com', from: "#{Settings.general.name} Support ", subject: "Congratulations on your first charge on #{Settings.general.name}!") end def welcome(np_id) @@ -111,8 +93,7 @@ def welcome(np_id) @user = @nonprofit.users.first @token = @user.make_confirmation_token! @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") + 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 28b7d52d8..6f476afff 100644 --- a/app/mailers/payment_mailer.rb +++ b/app/mailers/payment_mailer.rb @@ -6,9 +6,11 @@ class PaymentMailer < BaseMailer def resend_admin_receipt(payment_id, user_id) payment = Payment.find(payment_id) if payment.kind == 'Donation' || payment.kind == 'RecurringDonation' - return Delayed::Job.enqueue JobTypes::NonprofitPaymentNotificationJob.new(payment.donation.id, user_id) + return JobQueue.queue(JobTypes::NonprofitPaymentNotificationJob, payment.donation.id, payment_id, user_id) elsif payment.kind == 'Ticket' - return TicketMailer.receipt_admin(payment.donation.id, user_id).deliver + return JobQueue.queue(JobTypes::TicketMailerReceiptAdminJob, payment.tickets.pluck(:id), user_id) + elsif payment.kind == 'Refund' + return Delayed::Job.enqueue JobTypes::NonprofitRefundNotificationJob.new(payment.refund.id, user_id) end end @@ -17,9 +19,11 @@ def resend_admin_receipt(payment_id, user_id) def resend_donor_receipt(payment_id) payment = Payment.find(payment_id) if payment.kind == 'Donation' || payment.kind == 'RecurringDonation' - Delayed::Job.enqueue JobTypes::DonorPaymentNotificationJob.new(payment.donation.id) + return JobQueue.queue(JobTypes::DonorPaymentNotificationJob, payment.donation.id, payment.id) elsif payment.kind == 'Ticket' return TicketMailer.followup(payment.tickets.pluck(:id), payment.charge.id).deliver + elsif payment.kind == 'Refund' + return Delayed::Job.enqueue JobTypes::DonorRefundNotificationJob.new(payment.refund.id) end end diff --git a/app/mailers/stripe_account_mailer.rb b/app/mailers/stripe_account_mailer.rb new file mode 100644 index 000000000..433827698 --- /dev/null +++ b/app/mailers/stripe_account_mailer.rb @@ -0,0 +1,93 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StripeAccountMailer < BaseMailer + + def verified(nonprofit) + @nonprofit = nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + mail(to: @emails, subject: "Verification successful on #{Settings.general.name}!", + template_name: 'verified') + end + + def conditionally_send_verified(stripe_account) + @nonprofit = stripe_account&.nonprofit + if @nonprofit + verified(@nonprofit) + end + end + + def more_info_needed(nonprofit, deadline=nil) + @nonprofit = nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + @deadline = deadline.in_time_zone(@nonprofit.timezone).strftime('%B%e, %Y at%l:%M:%S %p') if deadline + mail(to: @emails, subject: "Urgent: More Info Needed for Your #{Settings.general.name} Verification", + template_name: 'more_info_needed') + end + + def conditionally_send_more_info_needed(stripe_account, email_to_send_guid, override=false) + conditionally_send(stripe_account, email_to_send_guid, override) do |stripe_account| + if (stripe_account&.nonprofit) + more_info_needed(stripe_account.nonprofit).deliver + end + end + end + + def not_completed(nonprofit, deadline=nil) + @nonprofit = nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + @deadline = deadline.in_time_zone(@nonprofit.timezone).strftime('%B%e, %Y at%l:%M:%S %p') if deadline + mail(to: @emails, subject: "Please Complete Your #{Settings.general.name} Account Verification", + template_name: 'not_completed') + end + + def conditionally_send_not_completed(stripe_account, email_to_send_guid, override=false) + conditionally_send(stripe_account, email_to_send_guid, override) do |stripe_account| + if stripe_account&.nonprofit + more_info_needed(stripe_account.nonprofit).deliver + end + end + end + + def no_longer_verified(nonprofit, deadline=nil) + @nonprofit = nonprofit + @emails = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_payouts') + @deadline = deadline.in_time_zone(@nonprofit.timezone).strftime('%B%e, %Y at%l:%M:%S %p') if deadline + mail(to: @emails, subject: "Additional account verification needed for #{Settings.general.name}", + template_name: 'no_longer_verified') + end + + def conditionally_send_no_longer_verified(stripe_account) + if stripe_account&.nonprofit + no_longer_verified(stripe_account.nonprofit, stripe_account.deadline).deliver + end + end + + private + def conditionally_send(stripe_account, email_to_send_guid, override=false, &block) + result = nil + if stripe_account&.nonprofit && + (stripe_account + &.nonprofit_verification_process_status || override) + + if override + result = block.call(stripe_account) + else + stripe_account + .nonprofit_verification_process_status + .with_lock("FOR UPDATE") do + if (stripe_account.nonprofit_verification_process_status + .email_to_send_guid == email_to_send_guid || override) + result = block.call(stripe_account) + end + end + end + result + end + end + + def conditionally_send_on_stripe(stripe_account, &block) + if stripe_account&.nonprofit + block.call(stripe_account) + end + end + +end diff --git a/app/mailers/tax_mailer.rb b/app/mailers/tax_mailer.rb new file mode 100644 index 000000000..1e8210ff3 --- /dev/null +++ b/app/mailers/tax_mailer.rb @@ -0,0 +1,19 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class TaxMailer < ApplicationMailer + + # Subject can be set in your I18n file at config/locales/en.yml + # with the following lookup: + # + # en.tax_mailer.annual_receipt.subject + # + def annual_receipt(supporter:, payments:, year:, nonprofit_text:) + @supporter = supporter + @nonprofit = supporter.nonprofit + @payments = payments + @year = year + @tax_id = supporter.nonprofit.ein + @nonprofit_text = nonprofit_text + + mail(to: @supporter.email, subject: "#{@year} Tax Receipt from #{@nonprofit.name}") + end +end diff --git a/app/mailers/ticket_mailer.rb b/app/mailers/ticket_mailer.rb index 623646d69..1f070b96e 100644 --- a/app/mailers/ticket_mailer.rb +++ b/app/mailers/ticket_mailer.rb @@ -12,7 +12,9 @@ def followup(ticket_ids, charge_id=nil) @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}") + unless @supporter.email.blank? + mail(from: from, to: @supporter.email, reply_to: reply_to, subject: "Your tickets#{@charge ? ' and receipt ' : ' '}for: #{@event.name}") + end end def receipt_admin(ticket_ids, user_id=nil) @@ -24,10 +26,11 @@ def receipt_admin(ticket_ids, user_id=nil) recipients = QueryUsers.nonprofit_user_emails(@nonprofit.id, 'notify_events') 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}") + if recipients.any? + mail(to: recipients, subject: "Ticket redeemed for #{@event.name} - #{@supporter.name}") + end end end diff --git a/app/models/activity.rb b/app/models/activity.rb index c98f08882..4b8b031c7 100644 --- a/app/models/activity.rb +++ b/app/models/activity.rb @@ -1,5 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Activity < ActiveRecord::Base - +class Activity < ApplicationRecord + belongs_to :attachment, :polymorphic => true + belongs_to :supporter + belongs_to :nonprofit end diff --git a/app/models/amount.rb b/app/models/amount.rb new file mode 100644 index 000000000..9fa9bec9d --- /dev/null +++ b/app/models/amount.rb @@ -0,0 +1,8 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# a simple object for storing values. Likely will be replaced with Money from ruby-money +# in future +Amount = Struct.new(:cents, :currency) diff --git a/app/models/application_record.rb b/app/models/application_record.rb new file mode 100644 index 000000000..2a20b64e2 --- /dev/null +++ b/app/models/application_record.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +class ApplicationRecord < ActiveRecord::Base + self.abstract_class = true + + include Model::Houidable + include Model::AsMoneyable +end diff --git a/app/models/bank_account.rb b/app/models/bank_account.rb index 92265f4f8..9f4a6ce0e 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") @@ -38,4 +38,8 @@ def invalidate! @not_valid = true end + def allows_payout? + !pending_verification && !deleted + end + end diff --git a/app/models/billing_plan.rb b/app/models/billing_plan.rb index 552a273d0..c646cbcf0 100644 --- a/app/models/billing_plan.rb +++ b/app/models/billing_plan.rb @@ -1,18 +1,44 @@ # 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 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 + :percentage_fee, # 0.038 + :flat_fee has_many :billing_subscriptions validates :name, :presence => true validates :amount, :presence => true + validates :percentage_fee, presence: true + + validates_numericality_of :amount, greater_than_or_equal_to: 0 + validates_numericality_of :percentage_fee, less_than: 1, greater_than_or_equal_to: 0 + + validates_numericality_of :flat_fee, only_integer: true, greater_than_or_equal_to: 0 + + concerning :PathCaching do + class_methods do + def clear_cache(np) + Rails.cache.delete(BillingPlan.create_cache_key(np)) + end + + def find_via_cached_np_id(np) + np = Nonprofit.find_via_cached_id(np.id) unless np.is_a? Nonprofit + key = BillingPlan.create_cache_key(np) + Rails.cache.fetch(key, expires_in: 4.hours) do + np.billing_plan + end + end + + def create_cache_key(np) + np = np.id if np.is_a? Nonprofit + "billing_plan_nonprofit_id_#{np}" + end + end + end end diff --git a/app/models/billing_subscription.rb b/app/models/billing_subscription.rb index 5e67bfea8..d3dcb63dd 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, @@ -21,11 +21,36 @@ def as_json(options={}) 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 + def stripe_subscription + Stripe::Subscription.retrieve(stripe_subscription_id) end + concerning :PathCaching do + included do + after_save do + nonprofit.clear_cache + true + end + end + + class_methods do + def clear_cache(np) + Rails.cache.delete(BillingSubscription.create_cache_key(np)) + end + + def find_via_cached_np_id(np) + np = np.id if np.is_a? Nonprofit + key = BillingSubscription.create_cache_key(np) + Rails.cache.fetch(key, expires_in: 4.hours) do + Qx.fetch(:billing_subscriptions, {nonprofit_id: np}).last + end + end + + def create_cache_key(np) + np = np.id if np.is_a? Nonprofit + "billing_subscription_nonprofit_id_#{np}" + end + end + end end diff --git a/app/models/campaign.rb b/app/models/campaign.rb index d40378735..1fed3dcd6 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, @@ -20,7 +20,6 @@ class Campaign < ActiveRecord::Base :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 @@ -37,24 +36,35 @@ class Campaign < ActiveRecord::Base :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} + :presence => true, numericality: { + only_integer: true + } + validate :validate_goal_amount validates :name, :presence => true, :length => {:maximum => 60} validates :slug, uniqueness: {scope: :nonprofit_id, message: 'You already have a campaign with that URL.'}, presence: true + validates :starting_point, :presence => true, + :numericality => { :only_integer => true, :greater_than_or_equal_to => 0 } + attr_accessor :goal_amount_dollars + attr_accessible :starting_point, #integer, number of donors to start with + :goal_is_in_supporters #boolean, true if you want to measure success based on donors instead of amount + mount_uploader :main_image, CampaignMainImageUploader mount_uploader :background_image, CampaignBackgroundImageUploader mount_uploader :banner_image, CampaignBannerImageUploader has_many :donations + ## we already have a recurring_donations relationship but it's broken so we'll create one here just as a workaround + has_many :valid_rds, :through => :donations, source: :recurring_donation, class_name: 'RecurringDonation' has_many :charges, through: :donations has_many :payments, through: :donations has_many :campaign_gift_options @@ -62,10 +72,11 @@ class Campaign < ActiveRecord::Base 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_one :misc_campaign_info, dependent: :destroy + belongs_to :widget_description belongs_to :parent_campaign, class_name: 'Campaign' has_many :children_campaigns, class_name: 'Campaign', foreign_key: 'parent_campaign_id' @@ -82,6 +93,10 @@ class Campaign < ActiveRecord::Base if self.goal_amount_dollars.present? self.goal_amount = (self.goal_amount_dollars.gsub(',','').to_f * 100).to_i end + + unless (self.starting_point) + self.starting_point = 0 + end self end @@ -111,6 +126,8 @@ class Campaign < ActiveRecord::Base self end + after_update :send_campaign_updated + def set_defaults self.total_supporters = 1 @@ -174,11 +191,26 @@ def finished? self.end_datetime && self.end_datetime < Time.now end + def validate_goal_amount + + goal_amount = self.goal_amount || 0 + if (self.goal_is_in_supporters) + if (goal_amount < 1) + errors.add(:goal_amount, "must be greater than or equal to 1") + end + else + if (goal_amount < 99) + errors.add(:goal_amount, "must be greater than or equal to 99 cents") + end + end + end + def child_params 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 + reason_for_supporting metadata goal_is_in_supporters + widget_description_id ) attributes.except(*excluded_for_peer_to_peer) end @@ -190,4 +222,53 @@ def child_campaign? def parent_campaign? !child_campaign? end + + def params_to_copy_from_parent + params = %w( + tagline body video_url receipt_message youtube_video_id summary name + ) + + parent_campaign.attributes.slice(*params) + end + + def update_from_parent! + if child_campaign? + params_to_copy_from_parent.each do |k,v| + update_attribute(k, v) + end + [:main_image, :background_image, :banner_image].each do |i| + if parent_campaign.send("#{i.to_s}?") + update_attribute(i, parent_campaign.send(i)) unless !parent_campaign.send(i.to_s) rescue Aws::S3::Errors::NoSuchKey + else + self.send("remove_#{i.to_s}!") + end + end + save! + end + end + + def self.get_campaign_and_children(campaign) + where('campaigns.id = ? OR campaigns.parent_campaign_id = ? ',campaign, campaign) + end + + def hide_cover_fees? + nonprofit.hide_cover_fees? || misc_campaign_info&.hide_cover_fees_option + end + + def fee_coverage_option + @fee_coverage_option ||= misc_campaign_info&.fee_coverage_option_config || nonprofit.fee_coverage_option + end + + # generally, don't use + def fee_coverage_option=(option) + @fee_coverage_option = option + end + + def paused? + !!(misc_campaign_info&.paused) + end + + def send_campaign_updated + JobQueue.queue(JobTypes::CampaignUpdatedJob, self.id) + end end diff --git a/app/models/campaign_gift.rb b/app/models/campaign_gift.rb index be8e76ba8..273cf9f27 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 f69129c4c..e320c33e6 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) @@ -16,6 +16,7 @@ class CampaignGiftOption < ActiveRecord::Base belongs_to :campaign has_many :campaign_gifts has_many :donations, through: :campaign_gifts + has_one :nonprofit, through: :campaign validates :name, presence: true validates :campaign, presence: true @@ -26,6 +27,10 @@ def total_gifts return self.campaign_gifts.count end + def gifts_available? + quantity.nil? || quantity.zero? || total_gifts < quantity + end + def as_json(options={}) h = super(options) h[:total_gifts] = self.total_gifts diff --git a/app/models/card.rb b/app/models/card.rb index d7b806f85..a5894c938 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) @@ -13,6 +13,11 @@ class Card < ActiveRecord::Base :holder, :holder_id, :holder_type, # polymorphic cardholder association :inactive # a card is inactive. This is currently only meaningful for nonprofit cards + scope :amex_only, -> { where('cards.name ILIKE ? OR cards.name ILIKE ?', 'American Express%', 'amex%') } + scope :not_amex, -> { where('cards.name NOT ILIKE ? AND cards.name NOT ILIKE ?', 'American Express%', 'amex%') } + + scope :held_by_nonprofits, -> { where('cards.holder_type = ? ', 'Nonprofit') } + scope :held_by_supporters, -> { where('cards.holder_type = ? ', 'Supporter') } attr_accessor :failure_message @@ -20,6 +25,70 @@ class Card < ActiveRecord::Base belongs_to :holder, polymorphic: true has_many :charges has_many :donations + has_many :recurring_donations, through: :donations has_many :tickets + has_one :source_token, as: :tokenizable + + + # an helpful method for getting the supporter when it's the holder + def supporter + if holder_type == 'Supporter' + holder + else + nil + end + end + + # an helpful method for getting the supporter's nonprofit + # when the supporter is the holder + def supporter_nonprofit + supporter&.nonprofit + end + + def amex? + !!(name =~ /American Express.*/i) + end + + def not_amex? + !amex? + end + + def stripe_customer + @stripe_customer ||= Stripe::Customer.retrieve(stripe_customer_id) + end + + def stripe_card + @stripe_card ||= stripe_customer.sources.retrieve(stripe_card_id) + end + + + concerning :Maintenance do + included do + # is this originally from balanced? + scope :legacy_balanced, -> {where('cards.stripe_customer_id ILIKE ?', '%balanced%')} + # Is this originally not from balanced + scope :not_legacy_balanced, -> {where('cards.stripe_customer_id NOT ILIKE ?', '%balanced%')} + # is this card unused + scope :unused, -> { references(:charges,:donations, :tickets).includes(:charges, :donations, :tickets).where('donations.id IS NULL AND charges.id IS NULL AND tickets.id IS NULL')} + + # these are stripe_card_ids which are on multiple cards + scope :nonunique_stripe_card_ids, -> {where('stripe_card_id IS NOT NULL').group('stripe_card_id').having('COUNT(id) > 1').select('stripe_card_id, COUNT(id)') } + + # cards we feel we can detach from Stripe due to nonuse + # this are cards which: + # * have a unique stripe card id (not on another Card object) + # * owned by a Supporter + # * never been on associated with a charge, donation or ticket + # * was created more than a month ago + # * not originally from the Balanced service used before Stripe + def self.detachable_because_of_nonuse + # we want cards which are: + possible_cards = not_legacy_balanced.unused.held_by_supporters.where('cards.created_at < ?', 1.month.ago).where('cards.stripe_card_id IS NOT NULL') + + nonunique_ids = nonunique_stripe_card_ids.map{|i| i.stripe_card_id} + cards_without_unique_stripe_card_ids = possible_cards.select{|i| !nonunique_ids.include? i.stripe_card_id} + end + end + end end diff --git a/app/models/charge.rb b/app/models/charge.rb index d2ce3f470..a11bde269 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, @@ -12,6 +12,7 @@ class Charge < ActiveRecord::Base has_one :campaign, through: :donation has_one :recurring_donation, through: :donation + has_one :stripe_dispute, primary_key: :stripe_charge_id, foreign_key: :stripe_charge_id has_many :tickets has_many :events, through: :tickets has_many :refunds @@ -22,6 +23,7 @@ class Charge < ActiveRecord::Base belongs_to :nonprofit belongs_to :donation belongs_to :payment + belongs_to :stripe_charge_object, primary_key: "stripe_charge_id", foreign_key: "stripe_charge_id", class_name: "StripeCharge" scope :paid, ->{where(status: ["available", "pending", "disbursed"])} scope :not_paid, ->{where(status: [nil, "failed"])} @@ -29,8 +31,35 @@ class Charge < ActiveRecord::Base scope :pending, ->{where(status: "pending")} scope :disbursed, ->{where(status: "disbursed")} + has_many :manual_balance_adjustments, as: :entity + def paid? self.status.in?(%w[available pending disbursed]) end + # Is this charge disbursed as part of a payout? + def disbursed? + status == 'disbursed' + end + + def stripe_charge(*expand) + Stripe::Charge.retrieve({id: stripe_charge_id, expand: expand}) + end + + def stripe_fee + stripe_charge('balance_transaction').balance_transaction.fee + end + + + concerning :Maintenance do + included do + scope :with_missing_stripe_charge_objects, -> { where('charges.stripe_charge_id IS NOT NULL AND stripe_charges.id IS NULL').includes(:stripe_charge_object).references(:stripe_charges)} + end + end + + def calculate_stripe_fee + source = stripe_charge_object.stripe_object.source + stripe_fee = nonprofit.calculate_stripe_fee(source: source, amount: amount, at: created_at) + end + end diff --git a/app/models/comment.rb b/app/models/comment.rb index e0491d498..6f4b9e52c 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/concerns/model/as_moneyable.rb b/app/models/concerns/model/as_moneyable.rb new file mode 100644 index 000000000..fe4d40d29 --- /dev/null +++ b/app/models/concerns/model/as_moneyable.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Model::AsMoneyable + extend ActiveSupport::Concern + class_methods do + def moneyable_attributes + @moneyable_attributes ||= [] + end + + # For every attribute in attr, this creates a new getter with the postfix of `_as_money` that + # returns the the attribute as an Amount, with the currency from + # the currency attribute of the class. + def as_money(*attr) + attr.each do |a| + moneyable_attributes << a + class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition + def #{a}_as_money + Amount.new(#{a} || 0, currency) + end + RUBY + end + end + end +end \ No newline at end of file diff --git a/app/models/concerns/model/calculated_names.rb b/app/models/concerns/model/calculated_names.rb new file mode 100644 index 000000000..3d9a4735a --- /dev/null +++ b/app/models/concerns/model/calculated_names.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Model::CalculatedNames + extend ActiveSupport::Concern + included do + def calculated_first_name + name_parts = name&.strip&.split(' ')&.map(&:strip) + case name_parts&.count || 0 + when 0 + nil + when 1 + name_parts[0] + else + name_parts[0..-2].join(" ") + end + end + + def calculated_last_name + name_parts = name&.strip&.split(' ')&.map(&:strip) + case name_parts&.count || 0 + when 0 + nil + when 1 + nil + else + name_parts[-1] + end + end + end +end \ No newline at end of file diff --git a/app/models/concerns/model/created_timeable.rb b/app/models/concerns/model/created_timeable.rb new file mode 100644 index 000000000..1aa333eec --- /dev/null +++ b/app/models/concerns/model/created_timeable.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# this mixin provides a way to set the :created attribute when the model is initialized unless it's already been provided +module Model::CreatedTimeable + extend ActiveSupport::Concern + included do + after_initialize :set_created_if_needed + + private + + def set_created_if_needed + self[:created] = Time.current unless self[:created] + end + end +end diff --git a/app/models/concerns/model/houidable.rb b/app/models/concerns/model/houidable.rb new file mode 100644 index 000000000..3b0202ab4 --- /dev/null +++ b/app/models/concerns/model/houidable.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# rubocop:disable Layout/TrailingWhitespace # we do this becuase rubocop is bizarrely crashing on this file +module Model::Houidable + extend ActiveSupport::Concern + class_methods do + ### + # @description: Simplifies using HouIDs for an ActiveRecord class. A Houid (pronounced "Hoo-id") is a unique + # identifier for an object. Houids have the format of: prefix_{22 random alphanumeric characters}. A prefix + # consists of lowercase alphabetical characters. Each class must have its own unique prefix. All of the Houids + # generated for that class will use that prefix. + # + # Given a prefix, adds the following features to a ActiveRecord class: + # - Sets a HouID to the id after object initialization (on "after_initialize" callback) if + # it hasn't already been set + # - Adds a "before_houid_set" and "after_houid_set" callbacks in case you want do + # somethings before or after that happens + # - Adds "before_houid_set" and "after_houid_set" callbacks if you want to take actions around + # - Adds the following public class methods (and instance methods that delegate to this methods): + # - houid_prefix - returns the prefix as a symbol + # - generate_houid - creates a new HouID with given prefix + # - houid_attribute - the symbol of the attribute on this class that the Houid is assigned to. + # - Adds the following public instance method: + # - to_houid - returns the houid for the instance regardless of what `houid_attribute` is. + # @param prefix {string|Symbol}: the prefix for the HouIDs on this model + # @param houid_attribute {string|Symbol}: the attribute on this model to assign the Houid to. Defaults to :id. + ### + def setup_houid(prefix, houid_attribute = :id) + + ###### + # define_model_callbacks :houid_set + # after_initialize :add_houid + + # # The HouID prefix as a symbol + # def houid_prefix + # :supp + # end + + # # Generates a HouID using the provided houid_prefix + # def generate_houid + # houid_prefix.to_s + "_" + SecureRandom.alphanumeric(22) + # end + + # private + # def add_houid + # run_callbacks(:houid_set) do + # write_attribute(:id, self.generate_houid) unless read_attribute(:id) + # end + # end + ##### + class_eval <<-RUBY, __FILE__, __LINE__ + 1 # rubocop:disable Style/DocumentDynamicEvalDefinition + define_model_callbacks :houid_set + after_initialize :add_houid + + delegate :houid_prefix, :houid_attribute, :generate_houid, to: :class + + # The HouID prefix as a symbol + # def self.houid_prefix + # :supp + # end + + def self.houid_prefix + :#{prefix} + end + + def self.houid_attribute + :#{houid_attribute} + end + + # Generates a HouID using the provided houid_prefix + def self.generate_houid + houid_prefix.to_s + "_" + SecureRandom.alphanumeric(22) + end + + def to_houid + self.send(houid_attribute) + end + + private + def add_houid + run_callbacks(:houid_set) do + write_attribute(self.houid_attribute, self.generate_houid) unless read_attribute(self.houid_attribute) + end + end + RUBY + end + end +end + +# rubocop:enable Layout/TrailingWhitespace diff --git a/app/models/concerns/model/subtransactable.rb b/app/models/concerns/model/subtransactable.rb new file mode 100644 index 000000000..80c10e4e6 --- /dev/null +++ b/app/models/concerns/model/subtransactable.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Model::Subtransactable + extend ActiveSupport::Concern + + included do + include Model::Houidable + + has_one :subtransaction, as: :subtransactable, dependent: :nullify + has_one :trx, through: :subtransaction, class_name: 'Transaction' + has_one :supporter, through: :trx + has_one :nonprofit, through: :trx + + has_many :subtransaction_payments, -> { extending ModelExtensions::PaymentsExtension }, through: :subtransaction + + delegate :currency, to: :nonprofit + + # Handle a completed refund from a legacy Refund object + # Implement this in your specific subtransaction class if you want to use it. + def process_refund(refund) + raise NotImplementedError, + "You need to implement 'process_refund' in your specific subtransaction class" + end + end +end diff --git a/app/models/concerns/model/subtransaction_paymentable.rb b/app/models/concerns/model/subtransaction_paymentable.rb new file mode 100644 index 000000000..60444723f --- /dev/null +++ b/app/models/concerns/model/subtransaction_paymentable.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Model::SubtransactionPaymentable + extend ActiveSupport::Concern + + included do + include Model::Houidable + + has_one :subtransaction_payment, as: :paymentable, dependent: :destroy + has_one :trx, through: :subtransaction_payment + has_one :supporter, through: :subtransaction_payment + has_one :nonprofit, through: :subtransaction_payment + + has_one :subtransaction, through: :subtransaction_payment + + has_many :object_events, as: :event_entity + + delegate :currency, to: :nonprofit + end +end diff --git a/app/models/concerns/model/trx_assignable.rb b/app/models/concerns/model/trx_assignable.rb new file mode 100644 index 000000000..760ecd568 --- /dev/null +++ b/app/models/concerns/model/trx_assignable.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module Model::TrxAssignable + extend ActiveSupport::Concern + + included do + include Model::Houidable + + has_one :transaction_assignment, as: :assignable + has_one :trx, through: :transaction_assignment, class_name: 'Transaction', foreign_key: 'transaction_id' + has_one :supporter, through: :transaction_assignment + has_one :nonprofit, through: :transaction_assignment + + delegate :currency, to: :nonprofit + + has_many :object_events, as: :event_entity + end +end diff --git a/app/models/coupon.rb b/app/models/coupon.rb deleted file mode 100644 index 1377d5484..000000000 --- a/app/models/coupon.rb +++ /dev/null @@ -1,12 +0,0 @@ -# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Coupon < ActiveRecord::Base - 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 diff --git a/app/models/custom_field_join.rb b/app/models/custom_field_join.rb index c38e9331e..0d96a3788 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 ef6bb85cd..aea45a2a1 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, @@ -12,6 +12,7 @@ class CustomFieldMaster < ActiveRecord::Base belongs_to :nonprofit has_many :custom_field_joins, dependent: :destroy + has_many :supporters, through: :custom_field_joins scope :not_deleted, ->{where(deleted: [nil,false])} diff --git a/app/models/direct_debit_detail.rb b/app/models/direct_debit_detail.rb index ba8fbe67a..d4dbfdcb2 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 722768b30..b6e6e2ee9 100644 --- a/app/models/dispute.rb +++ b/app/models/dispute.rb @@ -1,19 +1,84 @@ # 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] - Statuses = [:needs_response, :under_review, :won, :lost, :lost_and_paid] + Statuses = [:needs_response, :under_review, :won, :lost] attr_accessible \ :gross_amount, # int :charge_id, :charge, - :payment_id, :payment, :status, - :reason + :reason, + :started_at + + attr_accessible \ + :withdrawal_transaction, + :reinstatement_transaction belongs_to :charge - belongs_to :payment + has_one :stripe_dispute, foreign_key: :stripe_dispute_id, primary_key: :stripe_dispute_id + has_many :dispute_transactions, -> { order("date ASC") } + + has_one :supporter, through: :charge + has_one :nonprofit, through: :charge + has_one :original_payment, through: :charge, source: :payment + + has_many :activities, as: :attachment do + def create(event_type, event_time, attributes=nil, options={}, &block) + attributes = proxy_association.owner.build_activity_attributes(event_type, event_time) .merge(attributes || {}) + proxy_association.create(attributes, options, &block) + end + + def build(event_type, event_time, attributes=nil, options={}, &block) + attributes = proxy_association.owner.build_activity_attributes(event_type, event_time).merge(attributes || {}) + proxy_association.build(attributes, options, &block) + end + end + + + def withdrawal_transaction + dispute_transactions&.first + end + + def reinstatement_transaction + ((dispute_transactions&.count == 2) && dispute_transactions[1]) || nil + end + + def build_activity_json(event_type) + dispute = self + original_payment = dispute.original_payment + case event_type + when 'DisputeCreated', 'DisputeUpdated', 'DisputeLost', 'DisputeWon' + return { + gross_amount: dispute.gross_amount, + reason: dispute.reason, + status: dispute.status, + original_id: original_payment.id, + original_kind: original_payment.kind, + original_gross_amount: original_payment.gross_amount, + original_date: original_payment.date, + started_at: dispute.started_at + } + else + raise RuntimeError, "#{event_type} is not a valid Dispute event type" + end + end + def build_activity_attributes(event_type, event_time) + dispute = self + case event_type + when 'DisputeCreated', 'DisputeUpdated', 'DisputeLost', 'DisputeWon' + return { + kind: event_type, + date: event_time, + nonprofit: dispute.nonprofit, + supporter: dispute.supporter, + json_data: build_activity_json(event_type) + } + else + raise RuntimeError, "#{event_type} is not a valid Dispute event type" + end + end end diff --git a/app/models/dispute_payment_backup.rb b/app/models/dispute_payment_backup.rb new file mode 100644 index 000000000..ae7518a20 --- /dev/null +++ b/app/models/dispute_payment_backup.rb @@ -0,0 +1,5 @@ +class DisputePaymentBackup < ApplicationRecord + belongs_to :dispute + belongs_to :payment + attr_accessible :dispute, :payment_id +end diff --git a/app/models/dispute_transaction.rb b/app/models/dispute_transaction.rb new file mode 100644 index 000000000..d55aa0773 --- /dev/null +++ b/app/models/dispute_transaction.rb @@ -0,0 +1,25 @@ +class DisputeTransaction < ApplicationRecord + belongs_to :dispute + belongs_to :payment + attr_accessible :gross_amount, :disbursed, :payment, :fee_total, + :stripe_transaction_id, :date + + has_one :nonprofit, through: :dispute + has_one :supporter, through: :dispute + has_many :manual_balance_adjustments, as: :entity + + def gross_amount=(gross_amount) + write_attribute(:gross_amount, gross_amount) + calculate_net + end + + def fee_total=(fee_total) + write_attribute(:fee_total, fee_total) + calculate_net + end + + private + def calculate_net + self.net_amount = gross_amount + fee_total + end +end diff --git a/app/models/donation.rb b/app/models/donation.rb index d014f737e..6915a1433 100644 --- a/app/models/donation.rb +++ b/app/models/donation.rb @@ -1,5 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Donation < ActiveRecord::Base +class Donation < ApplicationRecord + + before_save :set_anonymous attr_accessible \ :date, # datetime (when this donation was made) @@ -21,11 +23,14 @@ class Donation < ActiveRecord::Base :direct_debit_detail_id, :direct_debit_detail, :payment_provider + # fts is generated via a trigger + attr_readonly :fts + 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 + validates :payment_provider, inclusion: { in: ['credit_card', 'sepa'] }, allow_blank: true has_many :charges has_many :campaign_gifts, dependent: :destroy @@ -36,6 +41,7 @@ class Donation < ActiveRecord::Base has_one :payment has_one :offsite_payment has_one :tracking + has_many :modern_donations belongs_to :supporter belongs_to :card belongs_to :direct_debit_detail @@ -45,4 +51,18 @@ class Donation < ActiveRecord::Base belongs_to :event scope :anonymous, -> {where(anonymous: true)} + + def campaign_gift_purchase? + campaign_gifts.any? + end + + def actual_donation? + campaign_gifts.none? + end + + private + + def set_anonymous + update_attributes(anonymous: false) if anonymous.nil? + end end diff --git a/app/models/drip_email_list.rb b/app/models/drip_email_list.rb new file mode 100644 index 000000000..8fd7bd6bd --- /dev/null +++ b/app/models/drip_email_list.rb @@ -0,0 +1,16 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +class DripEmailList < ApplicationRecord + + validates :mailchimp_list_id, :presence => true + + # the path on the Mailchimp api for the list + def list_path + "lists/#{mailchimp_list_id}" + end + + # the path on the Mailchimp api for the list's members + def list_members_path + list_path + "/members" + end +end diff --git a/app/models/e_tap_import.rb b/app/models/e_tap_import.rb new file mode 100644 index 000000000..dbdceb514 --- /dev/null +++ b/app/models/e_tap_import.rb @@ -0,0 +1,40 @@ +class ETapImport < ApplicationRecord + + attr_accessible :nonprofit + belongs_to :nonprofit + has_many :e_tap_import_journal_entries do + def create_from_csv(filename) + CSV.read(filename, headers:true).each do |row| + create(row: row.to_h) + end + end + end + has_many :e_tap_import_contacts do + def create_from_csv(filename) + CSV.read(filename, headers:true).each do |row| + create(row: row.to_h) + end + end + end + has_many :reassignments + + def self.create_import(nonprofit, journal_file, contacts_file) + + transaction do + e_tap_import = create(nonprofit:nonprofit) + e_tap_import.e_tap_import_contacts.create_from_csv(contacts_file) + e_tap_import.e_tap_import_journal_entries.create_from_csv(journal_file) + end + end + + + def process(user) + e_tap_import_journal_entries.order('e_tap_import_journal_entries.id ASC').each do |entry| + transaction do + if entry.unprocessed? + entry.to_wrapper.process(user) + end + end + end + end +end diff --git a/app/models/e_tap_import_contact.rb b/app/models/e_tap_import_contact.rb new file mode 100644 index 000000000..01d69b3d0 --- /dev/null +++ b/app/models/e_tap_import_contact.rb @@ -0,0 +1,266 @@ +class ETapImportContact < ApplicationRecord + attr_accessible :row, :nonprofit + belongs_to :e_tap_import + has_one :nonprofit, through: :e_tap_import + + def supporters + nonprofit.supporters.not_deleted.includes(:custom_field_joins => :custom_field_master) + .where('custom_field_masters.name = ?', "E-Tapestry Id #") + .where("custom_field_joins.value = ?", account_id.to_s).references(:custom_field_joins, :custom_field_masters) + end + + def supporter + supporters.first + end + + def self.with_supporters + select{|i| !i.supporter.nil?} + end + + def self.without_supporters + select{|i| i.supporter.nil?} + end + + def self.matched_by_address + cfm = CustomFieldMaster.find_by_name('Got Supporter by address') + + select{|i| i.supporter&.custom_field_joins&.select{|i| i.custom_field_master_id == cfm.id}&.any?} + end + + def self.find_by_account_id(account_id) + where("row @> '{\"Account Number\": \"#{account_id}\"}'").first + end + + def journal_entries + e_tap_import.e_tap_import_journal_entries.by_account(row['Account Number']) + end + + def self.find_by_account_name(account_name, account_email, original_account_id) + query = where("row @> '{\"Account Name\": \"#{account_name}\"}' OR row @> '{\"Email\": \"#{account_email}\"}' OR row @> '{\"Email Address 2\": \"#{account_email}\"}' OR row @> '{\"Email Address 3\": \"#{account_email}\"}'") + if account_email.blank? + query = where("row @> '{\"Account Name\": \"#{account_name}\"}'") + end + query.where("NOT row @> '{\"Account Number\": \"#{original_account_id}\"}'").first + end + + def create_or_update_CUSTOM(known_supporter=nil) + + custom_fields_to_save = self.to_custom_fields; + got_supporter_via_address = false + latest_journal_entry = journal_entries.first + + supporter = known_supporter || + self.supporter || + e_tap_import.nonprofit.supporters.not_deleted.where("name = ? AND LOWER(COALESCE(email, '')) = ?", self.name, self.email&.downcase).first + + if !supporter && !(self.address.blank? && self.state.blank? && self.city.blank?) + supporter = e_tap_import.nonprofit.supporters.not_deleted.where('name = ? AND address = ? AND state_code = ? AND city = ?',self.name,self.address,self.state, self.city).first + if supporter.present? + custom_fields_to_save = custom_fields_to_save + [['Got Supporter by address', "#{self.name}, #{self.address}, #{self.state}, #{self.city}"]] + got_supporter_via_address = true + end + end + + # is this also relate to the latest payment + if supporter + if (latest_journal_entry&.to_wrapper&.date || Time.at(0)) >= (supporter.payments.order('date DESC').first&.date || Time.at(0)) # + puts "update the supporter info" + begin + # did we overwrite the email? + if supporter.persisted? && supporter.email && to_supporter_args[:email] && supporter.email.downcase != to_supporter_args[:email].downcase + cfj = supporter.custom_field_joins.joins(:custom_field_master).where('custom_field_masters.name = ?', "Overwrote previous email").references(:custom_field_masters).first + val = (cfj&.split(',')|| []) + [supporter.email] + custom_fields_to_save = custom_fields_to_save + [['Overwrote previous email', val.join(',')]] + end + supporter.update(self.to_supporter_args) + rescue PG::NotNullViolation => e + byebug + raise e + end + else + puts "do nothing!" + end + else + supporter = e_tap_import.nonprofit.supporters.create(self.to_supporter_args) + end + + InsertCustomFieldJoins.find_or_create(e_tap_import.nonprofit.id, [supporter.id], custom_fields_to_save) if custom_fields_to_save.any? + supporter + end + + def journal_entries + e_tap_import.e_tap_import_journal_entries.find_all_by_contact(self) + end + + def name + row['Account Name'] || "" + end + + def account_id + row['Account Number'] + end + + def organization + row['Company'] + end + + def address + row['Parsed Address'] || "" + end + + def city + + row['Parsed City'] || "" + + end + + def zip_code + row['Parsed ZIP Code'] || "" + end + + def state + row['Parsed State'] || "" + end + + def country + row['Parsed Country'] || "" + end + + + + def email + if emails.count > 0 + emails[0] + else + nil + end + end + + + def email_address2 + if emails.count > 1 + emails[1] + else + nil + end; + end + + def email_address3 + if emails.count > 2 + emails[2] + else + nil + end + end + + def full_address + row['Full Address with Country (Single Line)'] || "" + end + + def church_parish + row['County'] + end + + def created_at + row['Creation Date'] + end + + def created_by + row['Created By'] + end + + def envelope_salutation + row["Envelope Salutation"] + end + + def supporter_phone + if phone_numbers.count > 0 + phone_numbers[0] + else + nil + end + end + + def supporter_phone_2 + if phone_numbers.count > 1 + phone_numbers[1] + else + nil + end + end + + def supporter_phone_3 + if phone_numbers.count > 2 + phone_numbers[2] + else + nil + end + end + + def to_supporter_args + supporter_args = { + email: email, + name: name, + organization: organization, + address: address, + city: city, + state_code: state, + country: country, + zip_code: zip_code + } + + unless supporter_phone.nil? + supporter_args = supporter_args.merge(phone: supporter_phone) + end + + supporter_args + end + + def to_custom_fields + custom_fields = [['E-Tapestry Id #', account_id]] + if supporter_phone_2 + custom_fields += [['Supporter Phone 2', supporter_phone_2]] + end + + if supporter_phone_3 + custom_fields += [['Supporter Phone 3', supporter_phone_3]] + end + + if email_address2 + custom_fields += [['Email Address 2', email_address2]] + end + + if email_address3 + custom_fields += [['Email Address 3', email_address3]] + end + + if church_parish + custom_fields += [['Church Parish', church_parish]] + end + + if envelope_salutation + custom_fields += [['Envelope Salutation', envelope_salutation]] + end + + if created_at + custom_fields += [['Created At', created_at]] + end + + if created_by + custom_fields += [['Created By', created_by]] + end + + custom_fields + end + + def emails + [row['Email Address 1'], row['Email Address 2'], row['Email Address 3']].select{|i| i.present?} + end + + private + + def phone_numbers + [row["Phone - Voice"], row['Phone - Mobile'], row['Phone - Cell']].select{|i| i.present?} + end + +end diff --git a/app/models/e_tap_import_journal_entry.rb b/app/models/e_tap_import_journal_entry.rb new file mode 100644 index 000000000..00607ef07 --- /dev/null +++ b/app/models/e_tap_import_journal_entry.rb @@ -0,0 +1,359 @@ +class ETapImportJournalEntry < ApplicationRecord + + def self.by_account(account_id) + where("row @> '{\"Account Number\": \"#{account_id}\"}'") + end + + def e_tap_import_contact + e_tap_import.e_tap_import_contacts.find_by_account_id(account_id) + end + + def supporter_through_e_tap_import_contact + e_tap_import_contact.supporter + end + + def supporters_through_journal_entries + journal_entries_to_items.map(&:item).map(&:supporter).uniq + end + + def account_id + row['Account Number'] + end + + module Common + module Payment + def designation + @row['Fund'] + end + + def campaign + @row['Campaign'] + end + + def approach + @row['Approach'] + end + + def letter + @row['Letter'] + end + + def to_payment_note + note_contents = [] + if campaign.present? + note_contents += ["Campaign: #{campaign}"] + end + if approach.present? + note_contents += ["Approach: #{approach}"] + end + + if letter.present? + note_contents += ["Letter: #{letter}"] + end + + note_contents.join(" \n") + end + + def corresponding_matches? + corresponding_payment.gross_amount == amount + end + + def create_or_update_payment + + # supporter = nil + # got_via_address = false + + if corresponding_payment&.supporter + supporter = @entry.contact.create_or_update_CUSTOM(corresponding_payment.supporter) + # byebug if supporter.id == 2362354 + ##sync_contact_with_supporter + else + ## create new supporter + supporter = @entry.contact.create_or_update_CUSTOM + # byebug if supporter.id == 2362354 + end + + + + + if corresponding_payment && corresponding_matches? + unless corresponding_payment.tickets.any? + byebug unless corresponding_payment.donation + UpdateDonation.update_payment(corresponding_payment.donation.id, { + designation: designation, + campaign_id: '', + event_id: '' + }.merge( + corresponding_payment&.donation&.comment ? { + comment: corresponding_payment.donation.comment + } : {} + ).with_indifferent_access + ) + end + + return corresponding_payment + else + result = InsertDonation.offsite({ + supporter_id:supporter.id, + nonprofit_id: @entry.e_tap_import.nonprofit.id, + date: date.to_s, + designation: designation, + amount: amount + }.with_indifferent_access.merge(self.methods.include?(:to_payment_note) ? {comment: to_payment_note} : {})) + return ::Payment.find(result[:json]['payment']['id']) + end + + end + + end + + module Pledge + def pledged + @row['Pledged'] + end + + def pledge_written_off + @row['Pledge Written Off?'] + end + end + + module Purchase + def authorization_code + gift_type_info['Authorization Code'] + end + + def corresponding_payment + if authorization_code.to_i != 0 + begin + @corresponding_payment ||= @np.payments.find(authorization_code.to_i) + rescue + @corresponding_payment ||= nil + end + else + @corresponding_payment ||= nil + end + end + + def amount + @row['Received'].gsub(/(\D|\.)/, '').to_i + end + + def gift_type_info + @row['Gift Type Information'].split(',').map(&:strip).map{|i| i.split(':').map(&:strip)}.map{|row| + + if row.count == 1 + [row[0], nil] + elsif row.count > 2 + [row[0], nil] + else + row + end + }.to_h + end + end + + end + + attr_accessible :row + has_many :journal_entries_to_items + + #has_many :items, through: :journal_entries_to_items, source: :item + belongs_to :e_tap_import + + scope :processed, -> {joins(:journal_entries_to_items)} + # scope :unprocessed, -> {includes(:journal_entries_to_items).where("journal_entries_to_items.id = null").references(:journal_entries_to_items)} + + + def unprocessed? + journal_entries_to_items.none? + end + + def to_wrapper + case type + when 'Note' + NoteRow.new self + when 'Contact' + ContactEvent.new self + when 'Calendar Item' + CalendarItem.new self + when 'Gift' + Gift.new self + when 'Payment' + CreditPurchase.new self + when 'Pledge' + Pledge.new self + when "Pledge / Payment" + PledgePayment.new self + end + end + + def self.find_all_by_contact(contact) + id = contact.is_a?(ETapImportContact) ? contact.account_id : contact + where("row->>? = ?", 'Account Number', id) + end + + def type + row['Type'] + end + + def contact + e_tap_import.e_tap_import_contacts.find_by_account_id(row['Account Number']) + end + + + + class RowWrapper + attr_accessor :entry + def initialize(entry) + @row = entry.row + @entry = entry + end + + def date + month, day, year = @row['Date'].split('/') + @date ||= ActiveSupport::TimeZone['Central Time (US & Canada)'].local(year, month, day) + end + + def supporter + entry.e_tap_import.nonprofit.supporters.not_deleted.includes(:custom_field_joins => :custom_field_master).where('custom_field_masters.name = ? AND custom_field_joins.value = ?', "E-Tapestry Id #", contact.id.to_s).references(:custom_field_masters, :custom_field_joins).first + end + + def contact + @entry.contact + end + + def find_or_create_supporter + supporter || contact.create_or_update_CUSTOM + end + end + + class NoteRow < RowWrapper + def note + @note ||= @row['Note'] + end + + def to_supporter_note + {created_at: date, content: note} + end + + def process(user) + sn = find_or_create_supporter.supporter_notes.build(user:user, **self.to_supporter_note.except(:created_at)) + + sn.created_at = self.to_supporter_note[:created_at] + sn.save! + @entry.journal_entries_to_items.create(item:sn) + end + end + + class ContactEvent < RowWrapper + def note + @note ||= @row['Note'] + end + + def subject + @row['Contact Subject'] + end + + def to_supporter_note + {created_at: date, content: "Subject: #{subject}, Note: #{note}"} + end + + def process(user) + + sn = find_or_create_supporter.supporter_notes.build(user:user, **self.to_supporter_note.except(:created_at)) + + sn.created_at = self.to_supporter_note[:created_at] + sn.save! + @entry.journal_entries_to_items.create(item:sn) + end + end + + class CalendarItem < RowWrapper + def to_supporter_note + {created_at: date, content: "Calendar Item"} + end + def process(user) + + sn = find_or_create_supporter.supporter_notes.build(user:user, **self.to_supporter_note.except(:created_at)) + + sn.created_at = self.to_supporter_note[:created_at] + sn.save! + @entry.journal_entries_to_items.create(item:sn) + end + end + + + + class Gift < RowWrapper + include ::ETapImportJournalEntry::Common::Payment + include ::ETapImportJournalEntry::Common::Purchase + def corresponding_payment + nil + end + + def process(user) + @entry.journal_entries_to_items.create(item:create_or_update_payment) + end + end + + + + class Pledge < RowWrapper + include ::ETapImportJournalEntry::Common::Pledge + + def to_supporter_note + content = "Pledged: #{pledged}" + if pledge_written_off.present? + content += "\nPledge Written Off? #{pledge_written_off}" + end + {created_at: date, content: content} + end + + def process(user) + + sn = find_or_create_supporter.supporter_notes.build(user:user, **self.to_supporter_note.except(:created_at)) + + sn.created_at = self.to_supporter_note[:created_at] + sn.save! + @entry.journal_entries_to_items.create(item:sn) + end + end + + + + class CreditPurchase < RowWrapper + + include Common::Purchase + include Common::Payment + + def process(user) + je_to_i= @entry.journal_entries_to_items.create(item: create_or_update_payment) + end + end + + class PledgePayment < RowWrapper + include Common::Purchase + include Common::Payment + include Common::Pledge + + def to_supporter_note + content = "Pledged: #{pledged}" + if pledge_written_off.present? + content += "\nPledged Written Off? #{pledge_written_off}" + end + {created_at: date, content: content} + end + + def process(user) + je_to_i= @entry.journal_entries_to_items.create(item: create_or_update_payment) + sn = je_to_i.item.supporter.supporter_notes.build(user:user, **self.to_supporter_note.except(:created_at)) + + sn.created_at = self.to_supporter_note[:created_at] + sn.save! + @entry.journal_entries_to_items.create(item:sn) + end + end + + + +end diff --git a/app/models/email_customization.rb b/app/models/email_customization.rb new file mode 100644 index 000000000..d959696b6 --- /dev/null +++ b/app/models/email_customization.rb @@ -0,0 +1,6 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class EmailCustomization < ApplicationRecord + belongs_to :nonprofit, required: true + + validates :name, :contents, presence: true +end diff --git a/app/models/email_draft.rb b/app/models/email_draft.rb index 9614c13c0..cc4951b0c 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 ea64e7738..5ef7045e5 100644 --- a/app/models/email_list.rb +++ b/app/models/email_list.rb @@ -1,6 +1,77 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class EmailList < ActiveRecord::Base - attr_accessible :list_name, :mailchimp_list_id, :nonprofit, :tag_master +class EmailList < ApplicationRecord belongs_to :nonprofit belongs_to :tag_master + + has_many :tag_joins, through: :tag_master + + has_many :supporters, through: :tag_joins + + # you can set this manually for testing but generally, it should be + # generated from the api key + attr_accessor :base_uri + + # the path on the Mailchimp api for the list + def list_path + "lists/#{mailchimp_list_id}" + end + + def list_url + base_uri + "/" + list_path + end + + # the path on the Mailchimp api for the list's members + def list_members_path + list_path + "/members" + end + + def list_members_url + base_uri + "/" + list_members_path + end + + # the Mailchimp api key we have saved for the nonprofit + def api_key + Mailchimp.get_mailchimp_token(nonprofit.id) + end + + # The base Mailchimp API uri. This includes getting the proper datacenter + # using the api key. + # + # NOTE: this value is cached. This is not an awful decision but could be + # easy to forget this + def base_uri + @base_uri ||= Mailchimp.base_uri(api_key) + end + + def active? + !deleted? + end + + # true if we no longer want to sync that list, false if we do + def deleted? + tag_master&.deleted + end + + # schedules a job to populate the list in the background + def populate_list_later + PopulateListJob.perform_later(self) + end + + # populate the list by adding every Supporter in the list to mailchimp + def populate_list + unless deleted? + Mailchimp.perform_batch_operations(nonprofit.id, supporters.all.map do |s| + build_supporter_post_operation(s) + end) + end + end + + def build_supporter_post_operation(supporter) + MailchimpBatchOperation.new(method: 'POST', list: self, supporter:supporter) + end + + # we don't currently use this but we could in the future + def build_supporter_delete_operation(supporter) + MailchimpBatchOperation.new(method: 'DELETE', list: self, supporter:supporter) + end end diff --git a/app/models/email_setting.rb b/app/models/email_setting.rb index c4f8bf2ec..0171f071d 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 4face4932..c67fd51bc 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 @@ -32,7 +32,8 @@ class Event < ActiveRecord::Base :nonprofit_id, # host :hide_title, # bool :organizer_email, # string - :receipt_message # text + :receipt_message, # text + :nonprofit validates :name, :presence => true validates :end_datetime, :presence => true @@ -48,6 +49,7 @@ class Event < ActiveRecord::Base belongs_to :profile has_many :donations has_many :charges, through: :tickets + has_many :ticketholders, through: :tickets, source: :supporter has_many :supporters, through: :donations has_many :recurring_donations has_many :ticket_levels, :dependent => :destroy @@ -55,9 +57,8 @@ class Event < ActiveRecord::Base 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_one :misc_event_info geocoded_by :full_address @@ -101,4 +102,11 @@ def full_address Format::Address.full_address(self.address, self.city, self.state_code, self.zip_code) end + def hide_cover_fees? + nonprofit.hide_cover_fees? || misc_event_info&.hide_cover_fees_option + end + + def get_tickets_button_label + misc_event_info&.custom_get_tickets_button_label || 'Get Tickets' + end end diff --git a/app/models/event_discount.rb b/app/models/event_discount.rb index 7e6a69a04..2888ecaa8 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 e49d5609a..ee4717a46 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/export_format.rb b/app/models/export_format.rb new file mode 100644 index 000000000..b7e38d5a6 --- /dev/null +++ b/app/models/export_format.rb @@ -0,0 +1,89 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class ExportFormat < ApplicationRecord + # name - string that refers to the name of the nonprofit + # date_format - a string that refers to the desired date format + # show_currency - boolean that decides whether the currency should be displayed or not + # custom_columns_and_values - customizes values and columns from the export + + belongs_to :nonprofit + + validates :name, presence: true + validates :nonprofit_id, presence: true + + validates_with PostgresqlDateFormatValidator, { attribute_name: :date_format } + + validate :valid_custom_columns_and_values? + + after_validation :normalize_custom_columns + + private + + ALLOWED_COLUMNS_TO_HAVE_NAMES_CUSTOMIZED = [ + 'payments.date', + 'payments.gross_amount', + 'payments.fee_total', + 'payments.net_amount', + 'payments.kind', + 'donations.anonymous', + 'supporters.anonymous', + 'donations.anonymous OR supporters.anonymous', + 'campaigns_for_export.name', + 'campaigns_for_export.id', + 'campaigns_for_export.creator_email', + 'campaign_gift_options.name', + 'events_for_export.name', + 'payments.id', + 'offsite_payments.check_number', + 'donations.comment', + 'misc_payment_infos.fee_covered', + 'donations.created_at' + ].freeze + + ALLOWED_COLUMNS_TO_HAVE_VALUES_CUSTOMIZED = [ + 'payments.kind', + 'donations.designation', + 'donations.anonymous', + 'supporters.anonymous', + 'donations.anonymous OR supporters.anonymous', + 'donations.comment', + 'campaigns_for_export.name', + 'campaign_gift_options.name', + 'events_for_export.name', + 'donations.comment', + 'misc_payment_infos.fee_covered' + ].freeze + + private_constant :ALLOWED_COLUMNS_TO_HAVE_NAMES_CUSTOMIZED + private_constant :ALLOWED_COLUMNS_TO_HAVE_VALUES_CUSTOMIZED + + def valid_custom_columns_and_values? + return if custom_columns_and_values.nil? + custom_columns_and_values.keys.each do |column| + if ALLOWED_COLUMNS_TO_HAVE_NAMES_CUSTOMIZED.include? column + unless (custom_columns_and_values[column].include? 'custom_name') || (custom_columns_and_values[column].include? 'custom_values') + errors.add(:custom_columns_and_values, "you need to include a 'custom_name' or 'custom_values' key to customize #{column} column") + end + if (!ALLOWED_COLUMNS_TO_HAVE_VALUES_CUSTOMIZED.include? column) && (custom_columns_and_values[column].include? 'custom_values') + errors.add(:custom_columns_and_values, "column #{column} can't have its values customized") + end + else + errors.add(:custom_columns_and_values, "column #{column} does not exist or is not available to be customized") + end + end + end + + def normalize_custom_columns + custom_columns_and_values&.each do |column, customizations| + customizations&.each do |customization, customization_subject| + if customization == 'custom_name' + custom_columns_and_values[column]['custom_name'] = + insert_trailing_double_quotes(customization_subject) + end + end + end + end + + def insert_trailing_double_quotes(value) + value.insert(0, '"').insert(-1, '"') + end +end diff --git a/app/models/fee_coverage_detail_base.rb b/app/models/fee_coverage_detail_base.rb new file mode 100644 index 000000000..55485021b --- /dev/null +++ b/app/models/fee_coverage_detail_base.rb @@ -0,0 +1,14 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# A FeeCoverageDetailBase describes the base amount for calculating fee coverage during a fee era. +# Notably, this entity only deals with the amount added from the fee_era itself for Stripe, +# NOT the amount added for Nonprofit's portion of the fees + +# @!attribute percentage_fee the percentage to be added for fee coverage during the given fee era +# @return [decimal] +# @!attribute flat_fee the amount in cents to be added for fee coverage during the given fee era +# @return [integer] +# @!attribute fee_era the fee era that this detail applies to. +# @return [FeeEra] +class FeeCoverageDetailBase < ApplicationRecord + belongs_to :fee_era, validate: true +end diff --git a/app/models/fee_era.rb b/app/models/fee_era.rb new file mode 100644 index 000000000..bf76eddc8 --- /dev/null +++ b/app/models/fee_era.rb @@ -0,0 +1,151 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# FeeEra describes a period of time where a given set of fee structures apply. +# +# @attribute! start_time +# @return [DateTime|nil] the time where the FeeEra should begin being applied. +# If nil, the era began at the earliest possible time. (Time.at(0)). At minimum, start_time or end_time must be set. + +# @attribute! end_time +# @return [DateTime|nil] the time where the FeeEra should stop being applied. +# If nil, the era ends at the latest possible time, i.e. way in the future. At minimum, start_time or end_time must be set. +# +# @attribute! fee_structures +# @return One or more FeeStructure objects that apply during the FeeEra +class FeeEra < ApplicationRecord + + has_one :fee_coverage_detail_base, validate: true + + has_many :fee_structures do + def find_by_source(source) + raise ArgumentError, + 'source must be a valid Stripe::Source, Stripe::Card or similar' unless source.respond_to?(:brand) and source.respond_to?(:country) + brand_found = select {|i| i.brand == source.brand}.first + return brand_found if brand_found + + blank_source = select{|i| i.brand.blank?}.first + raise ArgumentError, + 'source must be a valid Stripe::Source, Stripe::Card or similar' if blank_source.nil? + blank_source + end + end + + validates_associated :fee_structures, :fee_coverage_detail_base + + validates :international_surcharge_fee, + numericality: {greater_than_or_equal_to: 0, less_than: 1}, allow_nil: true + + validates_presence_of :international_surcharge_fee, if: -> { local_country.present? } + + validates_presence_of :fee_coverage_detail_base + # + # Should an international surcharge be added + # + # @param [#country] source the source which has a country to check against + # + # @return [Boolean] true if an international fee should be added, false otherwise + # + def charge_international_fee?(source) + local_country.present? && source.country != local_country + end + + # Whether the given time is included in the FeeEra. true if it does, false otherwise. + # @param at [DateTime,nil] + def in_era?(at=nil) + at ||= Time.current + test_start_time = start_time ||Time.at(0) + test_end_time = end_time || Time.new(9999,1) + (test_start_time...test_end_time).cover? at + end + + # Given a time, find the FeeEra that time is within. + # @param at [DateTime,nil] the time to use for searching for a FeeEra. Default of for current time + def self.find_by_time(at=nil) + at ||= Time.current + era_result = self.all.select{|i| i.in_era?(at)} + raise ActiveRecord::RecordNotFound if era_result.none? + era_result.first + end + + # Given a source, use the card network on the source in order to find the appropriate FeeStructure. + # The code works as follows: + # 1. Is there a FeeStructure in fee_structures with the brand of source? If so, we return that FeeStructure. + # 2. Return the FeeStructure in fee_structures which has no set brand. + # @param [#brand,#country] a Stripe::Source, Stripe::Card or similar + # @returns [FeeStructure] + def find_fee_structure_by_source(source) + fee_structures.find_by_source(source) + end + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Numeric] :platform_fee the platform percentage fee to add to the given fee structure + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [Integer] :flat_fee (0) the flat platform fee to add to the given fee structure + + def calculate_fee(opts={}) + find_fee_structure_by_source(opts[:source]).calculate_fee(opts) + end + + # @param [Hash] opts + # @option opts [#brand] :source the source to use for calculating the fee + # @option opts [Integer] :amount the amount of the transaction in cents + def calculate_stripe_fee(opts={}) + find_fee_structure_by_source(opts[:source]).calculate_stripe_fee(opts) + end + + # @param [Hash] opts + # @option opts [Stripe::Charge] :charge the Stripe::Charge to use for calculating the fee + # @option opts [Stripe::Refund] :refund the Stripe::Refund for + # @option opts [Stripe::ApplicationFee] :application_fee the Stripe::ApplicationFee for this Charge + def calculate_application_fee_refund(opts={}) + application_fee = opts[:application_fee] + charge = opts[:charge] + refund = opts[:refund] + stripe_fee_to_reserve = refund_stripe_fee? ? 0 : calculate_stripe_fee(amount: charge.amount, source: charge.source) + max_fee_to_refund = application_fee.amount - stripe_fee_to_reserve + refundable_fee_left = max_fee_to_refund - application_fee.amount_refunded + if (refundable_fee_left <= 0) + return 0 + end + + if (charge.refunded) + return refundable_fee_left + else + portion_of_charge_refunded = BigDecimal(refund.amount) / BigDecimal(charge.amount) + amount_to_refund = (BigDecimal(max_fee_to_refund) * portion_of_charge_refunded).floor + amount_to_refund >= refundable_fee_left ? refundable_fee_left : amount_to_refund + end + end + + # @param [Hash] opts + # @option opts [Time] :charge_date the date that the charge occurred for purposes of finding the correct fee era + # @option opts [Stripe::Charge] :charge the Stripe::Charge to use for calculating the fee + # @option opts [Stripe::Refund] :refund the Stripe::Refund for + # @option opts [Stripe::ApplicationFee] :application_fee the Stripe::ApplicationFee for this Charge + def self.calculate_application_fee_refund(opts={}) + FeeEra.find_by_time(opts[:charge_date]).calculate_application_fee_refund(opts) + end + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Numeric] :platform_fee the platform percentage fee to add to the given fee structure + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [Integer] :flat_fee (0) the flat platform fee to add to the given fee structure + # @option opts [DateTime,nil] :at (nil) the time to use for searching for a FeeEra. Default of current time + def self.calculate_fee(opts={}) + FeeEra.find_by_time(opts[:at]).calculate_fee(opts) + end + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [DateTime,nil] :at (nil) the time to use for searching for a FeeEra. Default of current time + def self.calculate_stripe_fee(opts={}) + FeeEra.find_by_time(opts[:at]).calculate_stripe_fee(opts) + end + + def self.current + FeeEra.find_by_time + end +end \ No newline at end of file diff --git a/app/models/fee_structure.rb b/app/models/fee_structure.rb new file mode 100644 index 000000000..d10d0e5d3 --- /dev/null +++ b/app/models/fee_structure.rb @@ -0,0 +1,125 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# A FeeStructure summarizes a set of various Stripe rates and surcharges to use when applied to a card and a transaction. +# +# !@attribute brand +# @return [String,nil] the card brands to apply to cards to if this is the most specific brand provider in the set of FeeStructures for a FeeEra. +# If this is nil, this is the least specific FeeStructure. +# +# !@attribute stripe_fee +# @return [BigNumber] the base stripe percentage fee that should apply to charges using the FeeStructure +# +# !@attribute flat_fee +# @return [Integer] the flat fee in cents which should be applied to charges using this FeeStructure +# +# !@attribute [r] international_surcharge_fee +# @return [BigDecimal] the additional percentage which should apply to charges for cards which are not in local_country + +class FeeStructure < ApplicationRecord + belongs_to :fee_era + + validates :flat_fee, + numericality: {only_integer: true, greater_than_or_equal_to: 0}, + presence: true + + validates :stripe_fee, + numericality: {greater_than_or_equal_to: 0, less_than: 1}, + presence: true + + validates_presence_of :fee_era + + delegate :charge_international_fee?, :international_surcharge_fee, to: :fee_era + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Numeric] :platform_fee the platform percentage fee to add to the given fee structure + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [Integer] :flat_fee (0) the flat platform fee to add to the given fee structure + + def calculate_fee(opts={}) + FeeCalculation.calculate(opts.merge(fee_structure:self)) + end + + # @param [Hash] opts + # @option opts [#brand] :source the source to use for calculating the fee + # @option opts [Integer] :amount the amount of the transaction in cents + def calculate_stripe_fee(opts={}) + StripeFeeCalculation.calculate(opts.merge(fee_structure:self)) + end + + class FeeCalculation + include ActiveModel::Validations + attr_accessor :source, :platform_fee, :flat_fee, :amount, :fee_structure + + validates :source, :platform_fee, :amount, presence: true + validates_numericality_of :platform_fee, less_than: 1.0, greater_than_or_equal_to: 0.0 + validates_numericality_of :amount, greater_than: 0, is_integer: true + validate :validate_source_is_source_like + + def initialize(**args) + @platform_fee = args[:platform_fee] + @source = args[:source] + @flat_fee = args[:flat_fee] || 0 + @amount = args[:amount] + @fee_structure = args[:fee_structure] + end + + def calculate + raise ArgumentError.new(errors.full_messages) unless valid? + + fee_surcharge = fee_structure.stripe_fee + BigDecimal(platform_fee) + if fee_structure.charge_international_fee?(source) + fee_surcharge += fee_structure.international_surcharge_fee + end + + (BigDecimal(amount) * fee_surcharge).ceil + fee_structure.flat_fee + flat_fee + end + + def self.calculate(**args) + FeeCalculation.new(**args).calculate + end + + private + + def validate_source_is_source_like + errors.add(:source, 'must respond to #brand') unless source.respond_to?(:brand) + errors.add(:source, 'must respond to #country') unless source.respond_to?(:country) + end + end + + class StripeFeeCalculation + include ActiveModel::Validations + attr_accessor :source, :amount, :fee_structure + + validates :source, :amount, :fee_structure, presence: true + validates_numericality_of :amount, greater_than: 0, is_integer: true + validate :validate_source_is_source_like + + def initialize(**args) + @source = args[:source] + @amount = args[:amount] + @fee_structure = args[:fee_structure] + end + + def calculate + raise ArgumentError.new(errors.full_messages) unless valid? + fee_surcharge = fee_structure.stripe_fee + if fee_structure.charge_international_fee?(source) + fee_surcharge += fee_structure.international_surcharge_fee + end + + (BigDecimal(amount) * fee_surcharge).ceil + fee_structure.flat_fee + end + + def self.calculate(**args) + StripeFeeCalculation.new(**args).calculate + end + + private + + def validate_source_is_source_like + errors.add(:source, 'must respond to #brand') unless source.respond_to?(:brand) + errors.add(:source, 'must respond to #country') unless source.respond_to?(:country) + end + end +end diff --git a/app/models/full_contact_info.rb b/app/models/full_contact_info.rb index 712579fab..19c71e09f 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, @@ -15,9 +15,9 @@ class FullContactInfo < ActiveRecord::Base :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, dependent: :destroy + has_many :full_contact_social_profiles, dependent: :destroy + has_many :full_contact_orgs, dependent: :destroy + has_many :full_contact_topics, dependent: :destroy belongs_to :supporter end diff --git a/app/models/full_contact_job.rb b/app/models/full_contact_job.rb new file mode 100644 index 000000000..29247c8e9 --- /dev/null +++ b/app/models/full_contact_job.rb @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class FullContactJob < ApplicationRecord + belongs_to :supporter +end diff --git a/app/models/full_contact_org.rb b/app/models/full_contact_org.rb index a8b4ce1b7..05acfc9c8 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 258a764fc..b96a5bb7e 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 047160ea0..b98a4dd34 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 475f9c277..0839097bc 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 d6afb9a4e..642ba68c1 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 b4e3f681b..66a06071a 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/journal_entries_to_item.rb b/app/models/journal_entries_to_item.rb new file mode 100644 index 000000000..2c69cbb45 --- /dev/null +++ b/app/models/journal_entries_to_item.rb @@ -0,0 +1,5 @@ +class JournalEntriesToItem < ApplicationRecord + attr_accessible :item + belongs_to :e_tap_import_journal_entry + belongs_to :item, polymorphic: true +end \ No newline at end of file diff --git a/app/models/mailchimp_batch_operation.rb b/app/models/mailchimp_batch_operation.rb new file mode 100644 index 000000000..defe1d4e5 --- /dev/null +++ b/app/models/mailchimp_batch_operation.rb @@ -0,0 +1,37 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +# represents an operation using Mailchimp's batch subscribe/unsubscribe +# See more at: https://mailchimp.com/developer/marketing/api/list-members/ +class MailchimpBatchOperation + include ActiveModel::Model + + attr_accessor :method, # POST or DELETE + :list, # the EmailList you're applying this to + :supporter # the Supporter in question + + def email + supporter.email + end + + def body + method === "POST" ? Mailchimp::create_subscribe_body(supporter) : nil + end + + def path + path = list.list_members_path + path = path + "/#{Digest::MD5.hexdigest(email.downcase).to_s}" if method === "DELETE" + path + end + + def to_h + if (email) + result = {method: method, path: path} + if body + result[:body] = JSON::dump(body) + end + result + else + nil + end + end +end diff --git a/app/models/manual_balance_adjustment.rb b/app/models/manual_balance_adjustment.rb new file mode 100644 index 000000000..c41c299eb --- /dev/null +++ b/app/models/manual_balance_adjustment.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class ManualBalanceAdjustment < ApplicationRecord + belongs_to :entity, polymorphic: true, required:true + belongs_to :payment, required:true + has_one :supporter, through: :payment + has_one :nonprofit, through: :payment + + validates_presence_of :gross_amount, :fee_total, :net_amount + + before_validation :add_payment, on: :create + + scope :not_disbursed, ->{where(disbursed: [nil, false])} + scope :disbursed, ->{where(disbursed: [true])} + + def gross_amount=(gross_amount) + write_attribute(:gross_amount, gross_amount) + calculate_net + end + + def fee_total=(fee_total) + write_attribute(:fee_total, fee_total) + calculate_net + end + + private + def calculate_net + self.net_amount = (gross_amount || 0) + (fee_total || 0) + end + + + def add_payment + unless self.payment + if self.entity + build_payment(supporter:self.entity.supporter, nonprofit: self.entity.nonprofit, kind: "ManualAdjustment", + gross_amount: self.gross_amount || 0, + fee_total: self.fee_total || 0, + net_amount: self.net_amount, + date: Time.current + ) + end + end + end +end diff --git a/app/models/misc_campaign_info.rb b/app/models/misc_campaign_info.rb new file mode 100644 index 000000000..6b4bd0121 --- /dev/null +++ b/app/models/misc_campaign_info.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class MiscCampaignInfo < ApplicationRecord + belongs_to :campaign + + validates_inclusion_of :fee_coverage_option_config, in: ['auto', 'manual', 'none', nil] + + attr_accessible :manual_cover_fees, :paused +end diff --git a/app/models/misc_event_info.rb b/app/models/misc_event_info.rb new file mode 100644 index 000000000..6f9bee7bf --- /dev/null +++ b/app/models/misc_event_info.rb @@ -0,0 +1,4 @@ +class MiscEventInfo < ApplicationRecord + belongs_to :event + attr_accessible :hide_cover_fees_option, :custom_get_tickets_button_label +end diff --git a/app/models/misc_payment_info.rb b/app/models/misc_payment_info.rb new file mode 100644 index 000000000..0eedcac53 --- /dev/null +++ b/app/models/misc_payment_info.rb @@ -0,0 +1,5 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class MiscPaymentInfo < ApplicationRecord + belongs_to :payment + attr_accessible :fee_covered +end diff --git a/app/models/misc_recurring_donation_info.rb b/app/models/misc_recurring_donation_info.rb new file mode 100644 index 000000000..27aa94a57 --- /dev/null +++ b/app/models/misc_recurring_donation_info.rb @@ -0,0 +1,5 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class MiscRecurringDonationInfo < ApplicationRecord + belongs_to :recurring_donation + attr_accessible :fee_covered +end diff --git a/app/models/misc_refund_info.rb b/app/models/misc_refund_info.rb new file mode 100644 index 000000000..075a103e1 --- /dev/null +++ b/app/models/misc_refund_info.rb @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class MiscRefundInfo < ApplicationRecord + belongs_to :refund +end diff --git a/app/models/miscellaneous_np_info.rb b/app/models/miscellaneous_np_info.rb index 2b15029a5..a9df57519 100644 --- a/app/models/miscellaneous_np_info.rb +++ b/app/models/miscellaneous_np_info.rb @@ -1,9 +1,12 @@ # 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, - :change_amount_message + :change_amount_message, + :hide_cover_fees belongs_to :nonprofit + + validates_inclusion_of :fee_coverage_option_config, in: ['auto', 'manual', 'none', nil] end diff --git a/app/models/model_extensions/payments_extension.rb b/app/models/model_extensions/payments_extension.rb new file mode 100644 index 000000000..a78d97a8e --- /dev/null +++ b/app/models/model_extensions/payments_extension.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module ModelExtensions + module PaymentsExtension + include Model::AsMoneyable + + as_money :gross_amount, :net_amount, :fee_total + + delegate :currency, to: :owner + + def gross_amount + map(&:gross_amount).sum + end + + def net_amount + map(&:net_amount).sum + end + + def fee_total + map(&:fee_total).sum + end + + # orders payments without using SQL. Use this if you need them ordered + # but the payments haven't been saved yet. + def ordered + sort_by {|i| [i.legacy_payment.date, i.updated_at]}.reverse + end + + def owner + proxy_association.owner + end + + end +end \ No newline at end of file diff --git a/app/models/model_extensions/transaction_assignment/refund_extension.rb b/app/models/model_extensions/transaction_assignment/refund_extension.rb new file mode 100644 index 000000000..cb3d3cffc --- /dev/null +++ b/app/models/model_extensions/transaction_assignment/refund_extension.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +module ModelExtensions::TransactionAssignment::RefundExtension + def trx + proxy_association.owner + end + + def assignments + proxy_association.owner.transaction_assignments + end + + # Handle a completed refund from a legacy Refund object + def process_refund(refund) + donation = assignments.select{|i| i.assignable.is_a? ModernDonation}.first.assignable + donation.amount = trx.amount + donation.save! + end +end \ No newline at end of file diff --git a/app/models/modern_donation.rb b/app/models/modern_donation.rb new file mode 100644 index 000000000..5716e323f --- /dev/null +++ b/app/models/modern_donation.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# ModernDonation represents a single donation event associated with a transaction. Everytime a +# a recurring donation is ran, it will create a new ModernDonation. This varies from how +# Donation works. +class ModernDonation < ApplicationRecord + include Model::TrxAssignable + setup_houid :don, :houid + + # TODO must associate with events and campaigns somehow + + # NOTE: REMEMBER a Donation does not necessarily represent a single event. It could represent info + # about a recurring donation as well. No, this isn't great. + belongs_to :legacy_donation, class_name: 'Donation', foreign_key: :donation_id + + delegate :designation, :dedication, :comment, to: :legacy_donation + + as_money :amount + + def dedication + begin + JSON::parse legacy_donation.dedication + rescue + nil + end + end + + # REMEMBER: multiple ModernDonations could have the same legacy_id + def legacy_id + legacy_donation.id + end + + def publish_created + object_events.create( event_type: 'donation.created') + end + + def publish_updated + object_events.create( event_type: 'donation.updated') + end + + def publish_deleted + object_events.create( event_type: 'donation.deleted') + end +end diff --git a/app/models/nonprofit.rb b/app/models/nonprofit.rb index 421b59b18..aba65ad5e 100755 --- a/app/models/nonprofit.rb +++ b/app/models/nonprofit.rb @@ -1,5 +1,7 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Nonprofit < ActiveRecord::Base +class Nonprofit < ApplicationRecord + include Model::Houidable + setup_houid :np, :houid 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"] @@ -11,8 +13,6 @@ class Nonprofit < ActiveRecord::Base :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 @@ -57,7 +57,23 @@ class Nonprofit < ActiveRecord::Base has_many :refunds, through: :charges has_many :donations has_many :recurring_donations - has_many :payments + has_many :payments do + def pending + joins(:charges).where('charges.status = ?', 'pending') + end + + def pending_totals + net, gross = pending.pluck('SUM("payments"."net_amount") AS net, SUM("payments"."gross_amount") AS gross').first + {'net' => net, 'gross' => gross} + end + + def during_np_year(year) + proxy_association.owner.use_zone do + where('date >= ? and date < ?', Time.zone.local(year), Time.zone.local(year + 1)) + end + end + end + has_many :transactions, through: :supporters has_many :supporters, dependent: :destroy has_many :supporter_notes, through: :supporters has_many :profiles, through: :donations @@ -72,39 +88,63 @@ class Nonprofit < ActiveRecord::Base has_many :imports has_many :email_settings has_many :cards, as: :holder + has_many :supporter_cards, through: :supporters, source: :cards, class_name: 'Card' + has_many :periodic_reports + has_many :export_formats - has_one :bank_account, dependent: :destroy, conditions: "COALESCE(deleted, false) = false" + has_one :nonprofit_key + + has_many :email_lists + + has_one :bank_account, -> { where("COALESCE(bank_accounts.deleted, false) = false") }, dependent: :destroy has_one :billing_subscription, dependent: :destroy has_one :billing_plan, through: :billing_subscription has_one :miscellaneous_np_info + has_one :nonprofit_deactivation + has_one :stripe_account, foreign_key: :stripe_account_id, primary_key: :stripe_account_id + + has_many :associated_object_events, class_name: 'ObjectEvent' validates :name, presence: true validates :city, presence: true validates :state_code, presence: true validates :email, format: { with: Email::Regex }, allow_blank: true + validate :timezone_is_valid validates_uniqueness_of :slug, scope: [: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)} mount_uploader :main_image, NonprofitUploader - mount_uploader :second_image, NonprofitUploader - mount_uploader :third_image, NonprofitUploader mount_uploader :background_image, NonprofitBackgroundUploader mount_uploader :logo, NonprofitLogoUploader - serialize :achievements, Array - serialize :categories, Array - geocoded_by :full_address + + + before_validation(on: :create) do self.set_slugs self end + concerning :Path do + class_methods do + ModernParams = Struct.new(:to_param) + end + included do + # When you use a routing helper like `api_new_nonprofit_supporter``, you need to provide objects which have a `#to_param` + # method. By default that's set to the value of `#id`. In our case, for the api objects, we want the id to instead be + # the value of `#houid`. We can't override `to_param` though because we may use route helpers which expect `#to_param` to + # return the value of `#id`. This is the hacky workaround. + def to_modern_param + ModernParams.new(houid) + end + end + end + # Register (create) a nonprofit with an initial admin def self.register(user, params) np = self.create ConstructNonprofit.construct(user, params) @@ -121,14 +161,6 @@ def total_recurring recurring_donations.active.sum(:amount) end - 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)} - donation_history_monthly - end - def as_json(options = {}) h = super(options) h[:url] = self.url @@ -161,11 +193,26 @@ def total_raised QueryPayments.get_payout_totals( QueryPayments.ids_for_payout(self.id))['net_amount'] end - def can_make_payouts - self.vetted && - self.verification_status == 'verified' && - self.bank_account && - !self.bank_account.pending_verification + def admins + users.nonprofit_admins + end + + def associates + users.nonprofit_associates + end + + def can_make_payouts? + !!(vetted && bank_account && + !bank_account.deleted && + !bank_account.pending_verification && + stripe_account&.payouts_enabled && + !(nonprofit_deactivation&.deactivated)) + end + + def can_process_charge? + !!(vetted && + stripe_account&.charges_enabled && + !(nonprofit_deactivation&.deactivated)) end def active_cards @@ -198,4 +245,235 @@ def create_active_card(card_data) def currency_symbol Settings.intntl.all_currencies.find{|i| i.abbv.downcase == currency.downcase}&.symbol end + + def steps_to_payout + ret = [] + + ret.push({name: :verification, + status: stripe_account&.verification_status || + :unverified}) + + bank_status = nil + no_bank_account = !bank_account || bank_account.deleted + + pending_bank_account = bank_account&.pending_verification + + valid_bank_account = bank_account && bank_account.pending_verification + + if (no_bank_account) + bank_status= :no_bank_account + elsif(pending_bank_account) + bank_status=:pending_bank_account + else + bank_status=:valid_bank_account + end + + ret.push({name: :bank_account, status: bank_status}) + + ret.push({ + name: :vetted, + status: vetted + }) + ret + end + + def autocomplete_supporter_address? + !!(feature_flag_autocomplete_supporter_address && autocomplete_supporter_address) + end + + concerning :FeeCalculation do + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [DateTime,nil] :at (nil) the time to use for searching for a FeeEra. Default of current time + def calculate_fee(opts={}) + FeeEra.calculate_fee( + **opts, + platform_fee: billing_plan.percentage_fee.to_s, + flat_fee: billing_plan.flat_fee + ) + end + + + # @param [Hash] opts + # @option opts [#brand, #country] :source the source to use for calculating the fee + # @option opts [Integer] :amount the amount of the transaction in cents + # @option opts [DateTime,nil] :at (nil) the time to use for searching for a FeeEra. Default of current time + def calculate_stripe_fee(opts={}) + FeeEra.calculate_stripe_fee(opts) + end + + # @param [Hash] opts + # @option opts [Time] :charge_date the date that the charge occurred for purposes of finding the correct fee era + # @option opts [Stripe::Charge] :charge the Stripe::Charge to use for calculating the fee + # @option opts [Stripe::Refund] :refund the Stripe::Refund for + # @option opts [Stripe::ApplicationFee] :application_fee the Stripe::ApplicationFee for this Charge + def calculate_application_fee_refund(opts={}) + FeeEra.calculate_application_fee_refund(opts) + end + + # the flat_fee and percentage_fee to use for calculating fee coverage on transactions for this nonprofit + # + # These fee_coverage details are calcuated as follows: + # + # { + # flat_fee: flat_fee as part of BillingPlan (unless FeeCoverageDetailsBase.dont_consider_billing_plan is true) + flat_fee from FeeCoverageDetailBase on current FeeEra, + # percentage_fee: percentage_fee as part of BillingPlan (unless FeeCoverageDetailsBase.dont_consider_billing_plan is true) + percentage_fee from FeeCoverageDetailBase on current FeeEra + # } + def fee_coverage_details + return fee_coverage_details_no_billing_plan if !billing_plan + { + flat_fee: (FeeEra.current.fee_coverage_detail_base.dont_consider_billing_plan ? 0 : billing_plan.flat_fee) + FeeEra.current.fee_coverage_detail_base.flat_fee, + percentage_fee: (FeeEra.current.fee_coverage_detail_base.dont_consider_billing_plan ? 0: billing_plan.percentage_fee) + FeeEra.current.fee_coverage_detail_base.percentage_fee + } + end + + def fee_coverage_details_no_billing_plan + { + flat_fee: FeeEra.current.fee_coverage_detail_base.flat_fee, + percentage_fee: FeeEra.current.fee_coverage_detail_base.percentage_fee + } + end + + def fee_coverage_details_with_json_safe_keys + fee_coverage_details.transform_keys{|i| i.to_s.camelize(:lower)} + end + end + + concerning :Deactivation do + included do + define_model_callbacks :deactivate + + scope :activated, -> { includes(:nonprofit_deactivation).where('nonprofit_deactivations.nonprofit_id IS NULL OR NOT COALESCE(nonprofit_deactivations.deactivated, false)').references(:nonprofit_deactivations)} + scope :deactivated, -> { joins(:nonprofit_deactivation).where('nonprofit_deactivations.deactivated = true')} + end + # Deactivate a nonprofit + def deactivate! + transaction do + run_callbacks :deactivate do + self.nonprofit_deactivation ||= NonprofitDeactivation.new + self.nonprofit_deactivation.deactivated = true + self.nonprofit_deactivation.save! + end + end + end + + def deactivated? + !!nonprofit_deactivation&.deactivated + end + + def activated? + !deactivated? + end + end + + concerning :Publishing do + include Nonprofit::Deactivation + included do + before_deactivate :unpublish! + end + + def unpublish! + self.published = false + self.save! + end + end + + concerning :S3Keys do + included do + has_many :nonprofit_s3_keys + end + end + + concerning :DateAndTime do + # retrieve the ActiveSupport::TimeZone object for the Nonprofit + # @return ActiveSupport::TimeZone the object representing the nonprofits set timezone; otherwise UTC + def zone + (timezone.present? && ActiveSupport::TimeZone[timezone]) || Time.zone + end + + # use the Nonprofit's timezone in a block + def use_zone(&block) + Time.use_zone(zone) do + yield block + end + end + end + + def has_achievements? + achievements.is_a?(Array) && achievements.any? + end + + def hide_cover_fees? + miscellaneous_np_info&.hide_cover_fees + end + + def fee_coverage_option + @fee_coverage_option ||= miscellaneous_np_info&.fee_coverage_option_config || 'auto' + end + + # generally, don't use + def fee_coverage_option=(option) + @fee_coverage_option = option + end + + concerning :PathCaching do + included do + after_save do + self.clear_cache + self + end + end + class_methods do + def clear_caching(id, state_code, city, name) + Rails.cache.delete(Nonprofit::create_cache_key_for_id(id)) + Rails.cache.delete(Nonprofit::create_cache_key_for_location(state_code, city, name)) + BillingSubscription.clear_cache(id) + BillingPlan.clear_cache(id) + end + + def find_via_cached_id(id) + key = create_cache_key_for_id(id) + Rails.cache.fetch(key, expires_in: 4.hours) do + Nonprofit.find(id) + end + end + + def find_via_cached_key_for_location(state_code, city, name) + key = create_cache_key_for_location(state_code, city, name) + Rails.cache.fetch(key, expires_in: 4.hours) do + Nonprofit.where(:state_code_slug => state_code, :city_slug => city, :slug => name).last + end + end + + def create_cache_key_for_id(id) + "nonprofit__CACHE_KEY__ID___#{id}" + end + + def create_cache_key_for_location(state_code,city, name) + "nonprofit__CACHE_KEY__LOCATION___#{state_code}____#{city}___#{name}" + end + end + + def clear_cache + Nonprofit::clear_caching(id, state_code_slug, city_slug, slug) + end + end + + concerning :TaxReceipting do + def supporters_who_have_payments_during_year(year, tickets:false) + payments_during_year = self.payments.during_np_year(year) + unless tickets + payments_during_year = payments_during_year.where("kind IS NULL OR kind != ? ", "ticket") + end + payments_during_year.group("supporter_id").select('supporter_id, COUNT(id)').each.map(&:supporter) + end + end + + private + + def timezone_is_valid + timezone.blank? || ActiveSupport::TimeZone.all.map{ |t| t.tzinfo.name }.include?(timezone) || errors.add(:timezone, 'is not a valid timezone') + end end diff --git a/app/models/nonprofit_account.rb b/app/models/nonprofit_account.rb index a3bff211a..b5ee62d08 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/nonprofit_deactivation.rb b/app/models/nonprofit_deactivation.rb new file mode 100644 index 000000000..f25dc1c71 --- /dev/null +++ b/app/models/nonprofit_deactivation.rb @@ -0,0 +1,4 @@ +class NonprofitDeactivation < ApplicationRecord + belongs_to :nonprofit + attr_accessible :deactivated +end diff --git a/app/models/nonprofit_key.rb b/app/models/nonprofit_key.rb new file mode 100644 index 000000000..e208b9a71 --- /dev/null +++ b/app/models/nonprofit_key.rb @@ -0,0 +1,18 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +# This is really just for mailchimp keys. + +# Actually handled through InsertNonprofitKeys +class NonprofitKey < ApplicationRecord + belongs_to :nonprofit, required: true + + validates_presence_of :mailchimp_token + + def mailchimp_token + read_attribute(:mailchimp_token).nil? ? nil : Cypher.decrypt(read_attribute(:mailchimp_token)) + end + + def mailchimp_token=(access_token) + write_attribute(:mailchimp_token, access_token.nil? ? nil : Cypher.encrypt(access_token)) + end +end diff --git a/app/models/nonprofit_s3_key.rb b/app/models/nonprofit_s3_key.rb new file mode 100644 index 000000000..106faf5f9 --- /dev/null +++ b/app/models/nonprofit_s3_key.rb @@ -0,0 +1,23 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +class NonprofitS3Key < ApplicationRecord + belongs_to :nonprofit, required: true + + validates_presence_of :access_key_id, :secret_access_key, :bucket_name, :region + + def aws_client + ::Aws::S3::Client.new(credentials:credentials, region: region) + end + + def credentials + ::Aws::Credentials.new(access_key_id, secret_access_key) + end + + def s3_resource + ::Aws::S3::Resource.new(client: aws_client) + end + + def s3_bucket + s3_resource.bucket(bucket_name) + end +end diff --git a/app/models/nonprofit_verification_process_status.rb b/app/models/nonprofit_verification_process_status.rb new file mode 100644 index 000000000..a57a45d6e --- /dev/null +++ b/app/models/nonprofit_verification_process_status.rb @@ -0,0 +1,6 @@ +class NonprofitVerificationProcessStatus < ApplicationRecord + attr_accessible :started_at, :stripe_account_id + + belongs_to :stripe_account, foreign_key: :stripe_account_id, primary_key: :stripe_account_id + +end diff --git a/app/models/object_event.rb b/app/models/object_event.rb new file mode 100644 index 000000000..afa44727e --- /dev/null +++ b/app/models/object_event.rb @@ -0,0 +1,111 @@ +# @license License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +=begin +An ObjectEvent represents a change that was made to an Houdini object. For example, when a supporter +is created a "supporter.created" object event is created. Object event names are: + +1. lower case and snake case with no spaces +2. consist of the Houdini object type name (usually, but not always, the class name snake-cased) and +and the event name, both snake_cased, seperated by a period + +# How to set up object events for a class. + +Setting up object events is pretty straightforward. You have to do the following: + +## Make sure `#setup_houid` is run on your class. +TIP: have your model inherit from `ApplicationRecord` so `#setup_houid` is available. + +```ruby +# in app/models/family_object.rb +class FamilyObject < ApplicationRecord + setup_houid :familyobj, :houid +end +``` + +## Add a method on your class for the event and which creates a new ObjectEvent + +```ruby +# in app/models/family_object.rb +class FamilyObject < ApplicationRecord + setup_houid :familyobj, :houid + + def publish_created # recommended that the method be `publish_` + ObjectEvent.create(event_entity: self, event_type: 'family_object.created') + end +end +``` + +## Add a jbuilder template for generating the ObjectEvent JSON +Place a jbuilder partial template at `app/views/api_new//object_events/_base.json`, which calls the general Jbuilder partial for the class +and sets what attributes should be expanded. + +```ruby +# in app/views/api_new/class name, snakecased/object_events/_base.json + +json.partial! event_entity, # the object is always passed in as `event_entity` + as: :family_object, # this is the parameter that your class' partial uses + __expand: build_json_expansion_path_tree('parent') # this tells us what parts of the object should be expanded using JSON SPaths + +``` + +@see Controllers::ApiNew::JbuilderExpansions + +@!attribute [r] event_type + @return [String] the type of the ObjectEvent. If a supporter was created, this would be `supporter.created` +@!attribute [r] event_entity_houid + @return [String] the houid of the object that the event occurred on. Automatically set at creation. +@!attribute [r] created + @return [Time] the moment in UTC that the object event was created. Automatically set at creation. +@!attribute [r] object_json + @return [Hash] the json representing the event. Automatically set at creation. +=end +class ObjectEvent < ApplicationRecord + include Model::CreatedTimeable + setup_houid :evt, :houid + + # @option attributes [#to_houid] :event_entity the object that had an event + # @option attributes [String] :event_type the name of the event that occurred on an object + def initialize(attributes = nil, options = {}) + super(attributes, options) + end + # Event entity is the Houdini object associated with the object event + belongs_to :event_entity, polymorphic: true + # the nonprofit the event_entity belongs to + belongs_to :nonprofit + + # methods that relate to querying for a nonprofit's (ObjectEvent)s + concerning :Query do + + class_methods do + # Queries the database to find every ObjectEvent associated with a particular object + # @param [string] event_entity_houid the Houid of the object whose object events you want to find + def event_entity(event_entity_houid) + where(event_entity_houid:event_entity_houid) + end + + # Queries the database to find every ObjectEvent of a particular type + def event_types(types) + where('event_type IN (?)', types) + end + end + end + + before_validation do + write_attribute(:object_json, to_object) if event_entity + write_attribute(:nonprofit_id, event_entity&.nonprofit&.id) if event_entity.respond_to? :nonprofit + write_attribute(:event_entity_houid, event_entity&.houid) + end + + private + # + # Generates the JSON representing the [ObjectEvent] + # @return [String] the JSON representing the ObjectEvent + def to_object + ApiNew::ObjectEventsController.render 'api_new/object_events/generate', + assigns: { + object_event:self, + event_entity: event_entity, + partial_path: "api_new/#{event_entity.to_partial_path.split('/').delete_at(0)}/object_events/base" + } + end +end diff --git a/app/models/offline_transaction.rb b/app/models/offline_transaction.rb new file mode 100644 index 000000000..1666dfd3b --- /dev/null +++ b/app/models/offline_transaction.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +# rubocop:disable Metrics/BlockLength, Metrics/AbcSize, Metrics/MethodLength +class OfflineTransaction < ApplicationRecord + include Model::Subtransactable + delegate :created, to: :subtransaction + + delegate :net_amount, to: :subtransaction_payments + as_money :amount, :net_amount + + concerning :JBuilder do + included do + setup_houid :offlinetrx, :houid + end + end +end +# rubocop:enable all diff --git a/app/models/offline_transaction_charge.rb b/app/models/offline_transaction_charge.rb new file mode 100644 index 000000000..cd95a1605 --- /dev/null +++ b/app/models/offline_transaction_charge.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +# rubocop:disable Metrics/BlockLength, Metrics/AbcSize +class OfflineTransactionCharge < ApplicationRecord + include Model::SubtransactionPaymentable + has_one :legacy_payment, class_name: 'Payment', through: :subtransaction_payment + has_one :offsite_payment, through: :legacy_payment + + delegate :gross_amount, :net_amount, :fee_total, to: :legacy_payment + delegate :currency, to: :nonprofit + + as_money :gross_amount, :net_amount, :fee_total + + def created + legacy_payment.date + end + + def publish_created + object_events.create( event_type: 'offline_transaction_charge.created') + end + + def publish_updated + object_events.create( event_type: 'offline_transaction_charge.updated') + end + + def publish_deleted + object_events.create( event_type: 'offline_transaction_charge.deleted') + end + + concerning :JBuilder do + included do + setup_houid :offtrxchrg, :houid + end + end +end +# rubocop:enable all diff --git a/app/models/offsite_payment.rb b/app/models/offsite_payment.rb index 4e4cf03ad..9a729ed27 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 e916a5887..0efad06b0 100644 --- a/app/models/payment.rb +++ b/app/models/payment.rb @@ -3,27 +3,112 @@ # 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 + extend ActiveSupport::Concern + attr_accessible \ :towards, :gross_amount, :refund_total, :fee_total, + :net_amount, :kind, - :date + :date, + :nonprofit, + :nonprofit_id, + :supporter_id, + :supporter belongs_to :supporter belongs_to :nonprofit has_one :charge has_one :offsite_payment has_one :refund - has_one :dispute + has_one :dispute_transaction + has_many :disputes, through: :charge belongs_to :donation has_many :tickets has_one :campaign, through: :donation + has_many :campaign_gifts, through: :donation has_many :events, through: :tickets has_many :payment_payouts has_many :charges + has_one :misc_payment_info + has_one :journal_entries_to_item, as: :item + has_one :payment_dupe_status + has_one :manual_balance_adjustment + + has_one :subtransaction_payment, foreign_key: 'legacy_payment_id', inverse_of: :legacy_payment + + has_one :trx, class_name: 'Transaction', through: :subtransaction_payment + + has_many :activities, :as => :attachment do + def create(attributes=nil, options={}, &block) + attributes = proxy_association.owner.build_activity_attributes.merge(attributes || {}) + proxy_association.create(attributes, options, &block) + end + + def build(attributes=nil, options={}, &block) + attributes = proxy_association.owner.build_activity_attributes.merge(attributes || {}) + proxy_association.build(attributes, options, &block) + end + end + + + def staff_comment + (manual_balance_adjustment&.staff_comment&.present? && manual_balance_adjustment&.staff_comment) || nil + end + + + scope :anonymous, -> {includes(:donation, :supporter).where('donations.anonymous OR supporters.anonymous').references(:supporters, :donations)} + scope :not_anonymous, -> { includes(:donation, :supporter).where('NOT(donations.anonymous OR supporters.anonymous)').references(:supporters, :donations) } + scope :not_matched, -> { joins('LEFT JOIN payment_dupe_statuses ON payment_dupe_statuses.payment_id = payments.id').where('payment_dupe_statuses.id IS NULL OR NOT payment_dupe_statuses.matched') } + + def consider_anonymous? + !!(supporter&.anonymous || donation&.anonymous) + end + + def build_activity_json + dispute_transaction_payment = self + dispute = dispute_transaction_payment.dispute_transaction.dispute + original_payment = dispute.original_payment + case kind + when 'Dispute', 'DisputeReversed' + return { + gross_amount: dispute_transaction_payment.gross_amount, + fee_total: dispute_transaction_payment.fee_total, + net_amount: dispute_transaction_payment.net_amount, + reason: dispute.reason, + status: dispute.status, + original_id: original_payment.id, + original_kind: original_payment.kind, + original_gross_amount: original_payment.gross_amount, + original_date: original_payment.date, + started_at: dispute.started_at + } + end + end + def build_activity_attributes + dispute_transaction_payment = self + case kind + when 'Dispute' + return { + kind: 'DisputeFundsWithdrawn', + date: dispute_transaction_payment.date, + nonprofit: dispute_transaction_payment.nonprofit, + supporter: dispute_transaction_payment.supporter, + json_data: build_activity_json + } + when 'DisputeReversed' + return { + kind: 'DisputeFundsReinstated', + date: dispute_transaction_payment.date, + nonprofit: dispute_transaction_payment.nonprofit, + supporter: dispute_transaction_payment.supporter, + json_data: build_activity_json + } + end + end end diff --git a/app/models/payment_dupe_status.rb b/app/models/payment_dupe_status.rb new file mode 100644 index 000000000..7f21552a4 --- /dev/null +++ b/app/models/payment_dupe_status.rb @@ -0,0 +1,3 @@ +class PaymentDupeStatus < ApplicationRecord + belongs_to :payment +end diff --git a/app/models/payment_import.rb b/app/models/payment_import.rb index ebe96e119..925e0c38b 100644 --- a/app/models/payment_import.rb +++ b/app/models/payment_import.rb @@ -1,7 +1,7 @@ # 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 + has_and_belongs_to_many :donations, join_table: 'donations_payment_imports' belongs_to :nonprofit belongs_to :user end diff --git a/app/models/payment_payout.rb b/app/models/payment_payout.rb index a7a840d1c..3c686763d 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 aa53c033c..0b9cdb11b 100644 --- a/app/models/payout.rb +++ b/app/models/payout.rb @@ -4,7 +4,10 @@ # # These are tied to Stripe transfers -class Payout < ActiveRecord::Base +# Unless you're sure, DO NOT CREATE THESE USING STANDARD ACTIVERECORD METHODS. Use `InsertPayout.with_stripe` instead. +class Payout < ApplicationRecord + + setup_houid :pyout, :houid attr_accessible \ :scheduled, # bool (whether this was made automatically at the beginning of the month) @@ -25,6 +28,7 @@ class Payout < ActiveRecord::Base has_one :bank_account, through: :nonprofit has_many :payment_payouts has_many :payments, through: :payment_payouts + has_many :object_events, as: :event_entity validates :stripe_transfer_id, presence: true, uniqueness: true validates :nonprofit, presence: true @@ -35,9 +39,21 @@ class Payout < ActiveRecord::Base validate :nonprofit_must_have_identity_verified, on: :create validate :bank_account_must_be_confirmed, on: :create + delegate :currency, to: :nonprofit + + as_money :net_amount + scope :pending, -> {where(status: 'pending')} scope :paid, -> {where(status: ['paid', 'succeeded'])} + # Older transfers use the Stripe::Transfer object, newer use Stripe::Payout object + def transfer_type + if (stripe_transfer_id.start_with?('tr_') || stripe_transfer_id.start_with?('test_tr_')) + return :transfer + elsif (stripe_transfer_id.start_with?('po_') || stripe_transfer_id.start_with?('test_po_')) + return :payout + end + end def bank_account_must_be_confirmed if self.bank_account && self.bank_account.pending_verification @@ -46,12 +62,14 @@ def bank_account_must_be_confirmed end def nonprofit_must_have_identity_verified - self.errors.add(:nonprofit, "must be verified") unless self.nonprofit && self.nonprofit.verification_status == 'verified' + self.errors.add(:nonprofit, "must be verified") unless self.nonprofit && self.nonprofit&.stripe_account&.payouts_enabled end def nonprofit_must_be_vetted self.errors.add(:nonprofit, "must be vetted") unless self.nonprofit && self.nonprofit.vetted end + def publish_created + object_events.create(event_type: 'payout.created') + end end - diff --git a/app/models/periodic_report.rb b/app/models/periodic_report.rb new file mode 100644 index 000000000..f01fca3f5 --- /dev/null +++ b/app/models/periodic_report.rb @@ -0,0 +1,63 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PeriodicReport < ApplicationRecord + + # active # boolean, + # report_type # string, + # period # string, + # users, + # nonprofit_id + + belongs_to :nonprofit + has_and_belongs_to_many :users + belongs_to :nonprofit_s3_key + + validate :valid_report_type? + validate :valid_period? + validate :valid_nonprofit_s3_key? + validate :valid_users? + + + validates :nonprofit, presence: true + + scope :active, -> { where(active: true) } + + # run the report + delegate :run, to: :adapter + + private + + AVAILABLE_REPORT_TYPES = [:failed_recurring_donations, :cancelled_recurring_donations, :active_recurring_donations_to_csv, :started_recurring_donations_to_csv].freeze + AVAILABLE_PERIODS = [:last_month, :all].freeze + + private_constant :AVAILABLE_REPORT_TYPES + private_constant :AVAILABLE_PERIODS + + def valid_report_type? + errors.add(:report_type, 'must be a supported report type') unless AVAILABLE_REPORT_TYPES.include? report_type&.to_sym + end + + def valid_period? + errors.add(:period, 'must be a supported period') unless AVAILABLE_PERIODS.include? period&.to_sym + end + + def valid_nonprofit_s3_key? + errors.add(:nonprofit_s3_key, 'must belong to the nonprofit set via :nonprofit') if nonprofit_s3_key.present? && nonprofit_s3_key.nonprofit != nonprofit + end + + def valid_users? + errors.add(:users, 'must be a list of users') if self.users.none? + users_authorized_to_have_report? + end + + def users_authorized_to_have_report? + self.users.each do |user| + unless nonprofit.users.include?(user) || user.roles&.pluck(:name)&.include?('super_admin') + errors.add(:users, 'must be a user of the nonprofit or a super admin') + end + end + end + + def adapter + PeriodicReportAdapter.build({ report_type: report_type, nonprofit_id: nonprofit_id, period: period, users: users, nonprofit_s3_key: nonprofit_s3_key, filename:filename }) + end +end diff --git a/app/models/profile.rb b/app/models/profile.rb index 5ff2f392f..0dc5e4939 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 @@ -13,7 +13,6 @@ class Profile < ActiveRecord::Base :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, @@ -23,8 +22,6 @@ class Profile < ActiveRecord::Base attr_accessor :email, :city_state - serialize :privacy_settings, Array - mount_uploader :picture, ProfileUploader belongs_to :user @@ -34,7 +31,6 @@ class Profile < ActiveRecord::Base 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 @@ -86,11 +82,9 @@ def supporter_name end def get_profile_picture(size=:normal) - # Can be, in order of precedence: your uploaded photo, facebook picture, or + # Can be, in order of precedence: your uploaded photo or # default image - if self.user.picture - return self.user.get_picture(size) - else + if self.picture? return self.picture_url(size) end # Either does not want photo shown or has none uploaded. diff --git a/app/models/reassignment.rb b/app/models/reassignment.rb new file mode 100644 index 000000000..d980d58f8 --- /dev/null +++ b/app/models/reassignment.rb @@ -0,0 +1,6 @@ +class Reassignment < ApplicationRecord + belongs_to :item, polymorphic: true + belongs_to :e_tap_import + belongs_to :source_supporter, class_name: 'Supporter', foreign_key: 'source_supporter_id' + belongs_to :target_supporter, class_name: 'Supporter', foreign_key: 'target_supporter_id' +end diff --git a/app/models/recaptcha_rejection.rb b/app/models/recaptcha_rejection.rb new file mode 100644 index 000000000..e78a92246 --- /dev/null +++ b/app/models/recaptcha_rejection.rb @@ -0,0 +1,3 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class RecaptchaRejection < ApplicationRecord +end diff --git a/app/models/recurring_donation.rb b/app/models/recurring_donation.rb index 1a088b1e3..ac283e5e2 100644 --- a/app/models/recurring_donation.rb +++ b/app/models/recurring_donation.rb @@ -1,5 +1,30 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class RecurringDonation < ActiveRecord::Base +class RecurringDonation < ApplicationRecord + + # The status is confusing here. + # A recurring donation can be in one of four status' + # * active + # * cancelled + # * failed + # * fulfilled + + # the conditions for those statuses are as follows + # if the field 'active' is false, then "cancelled" + # else if n_failures is at least 3, then "failed" + # else if the end_date is set and in the past, "fulfilled" + # else "active" + + + # The query that displays in nonprofits/:id/recurring_donations is rds that are active OR failed + + + define_model_callbacks :cancel + + before_save :set_anonymous + + after_create :fire_recurring_donation_created + + after_cancel :fire_recurring_donation_cancelled attr_accessible \ :amount, # int (cents) @@ -15,18 +40,29 @@ class RecurringDonation < ActiveRecord::Base :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 + :supporter_id, #used because things are messed up in the datamodel + :anonymous scope :active, -> {where(active: true)} scope :inactive, -> {where(active: [false,nil])} + scope :cancelled, -> {where(active: [false, nil])} scope :monthly, -> {where(time_unit: 'month', interval: 1)} scope :annual, -> {where(time_unit: 'year', interval: 1)} + scope :failed, -> {where('n_failures >= 3')} + scope :unfailed, -> {where('n_failures < 3')} + scope :fulfilled, -> {where('recurring_donations.end_date < ?', Time.current.to_date)} + scope :unfulfilled, -> {where('recurring_donations.end_date IS NULL OR recurring_donations.end_date IS >= ?', Time.current.to_date)} + + scope :may_attempt_again, -> {where('recurring_donations.active AND (recurring_donations.end_date IS NULL OR recurring_donations.end_date > ?) AND recurring_donations.n_failures < 3', Time.current)} belongs_to :donation belongs_to :nonprofit has_many :charges, through: :donation has_one :card, through: :donation has_one :supporter, through: :donation + has_one :misc_recurring_donation_info + has_one :recurring_donation_hold + has_many :activities, :as => :attachment validates :paydate, numericality: {less_than: 29}, allow_blank: true validates :donation_id, presence: true @@ -55,6 +91,28 @@ def total_given end + def failed? + n_failures >= 3 + end + + def cancelled? + !active + end + + # will this recurring donation be attempted again the next time it should be run? + def will_attempt_again? + !failed? && !cancelled? && (end_date.nil? || end_date > Time.current); + end + + def cancel!(email) + run_callbacks(:cancel) do + self.active = false + self.cancelled_by = email + self.cancelled_at = Time.current + save! + end unless cancelled? + end + # XXX let's make these monthly_totals a query # Or just push it into the front-end def self.monthly_total @@ -70,4 +128,17 @@ def monthly_total return self.donation.amount * multiple end + private + + def set_anonymous + update_attributes(anonymous: false) if anonymous.nil? + end + + def fire_recurring_donation_created + RecurringDonationCreatedJob.perform_later(self) + end + + def fire_recurring_donation_cancelled + RecurringDonationCancelledJob.perform_later(self) + end end diff --git a/app/models/recurring_donation_hold.rb b/app/models/recurring_donation_hold.rb new file mode 100644 index 000000000..753725cd5 --- /dev/null +++ b/app/models/recurring_donation_hold.rb @@ -0,0 +1,5 @@ +class RecurringDonationHold < ApplicationRecord + # attr_accessible :title, :body + attr_accessible :end_date + belongs_to :recurring_donation +end diff --git a/app/models/refund.rb b/app/models/refund.rb index f7fff991b..2a779b4e1 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] @@ -18,9 +18,15 @@ class Refund < ActiveRecord::Base belongs_to :charge belongs_to :payment + has_one :subtransaction_payment, through: :payment + has_one :misc_refund_info + has_one :nonprofit, through: :charge + has_one :supporter, through: :charge scope :not_disbursed, ->{where(disbursed: [nil, false])} scope :disbursed, ->{where(disbursed: [true])} + has_many :manual_balance_adjustments, as: :entity + end diff --git a/app/models/role.rb b/app/models/role.rb index 2c8401bf6..b4b73afa9 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 @@ -37,6 +37,9 @@ 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? + + MailchimpNonprofitUserAddJob.perform_later(user, nonprofit) + if user.confirmed? NonprofitAdminMailer.delay.existing_invite(role) else diff --git a/app/models/simple_object.rb b/app/models/simple_object.rb new file mode 100644 index 000000000..e8b57e82b --- /dev/null +++ b/app/models/simple_object.rb @@ -0,0 +1,21 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# +# ONLY USED FOR TESTING +# This file is basically a super simple mock for testing certain behaviors around events and relationship +# We use it in specs only +class SimpleObject < ApplicationRecord + include Model::Houidable + setup_houid :smplobj, :houid + belongs_to :parent, class_name: "SimpleObject" + belongs_to :nonprofit + + has_many :friends, class_name: "SimpleObject", foreign_key: 'friend_id' + + def publish_created + ObjectEvent.create(event_entity:self, event_type: 'simple_object.created') + end + + def publish_updated + ObjectEvent.create(event_entity:self, event_type: 'simple_object.updated') + end +end diff --git a/app/models/source_token.rb b/app/models/source_token.rb index 2d785ce15..8fd9a3264 100644 --- a/app/models/source_token.rb +++ b/app/models/source_token.rb @@ -1,7 +1,21 @@ # 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 belongs_to :event + + scope :expired, -> { where('max_uses <= total_uses OR expiration < ?', Time.now)} + scope :unexpired, -> { where(' NOT (max_uses <= total_uses OR expiration < ?)', Time.now) } + + scope :last_used_more_than_a_month_ago, -> {where('source_tokens.updated_at < ? ', 1.month.ago)} + + def expired? + max_uses <= total_uses || source_token.expiration < Time.now + end + + def unexpired? + !expired? + end end diff --git a/app/models/stripe_account.rb b/app/models/stripe_account.rb new file mode 100644 index 000000000..a15143d28 --- /dev/null +++ b/app/models/stripe_account.rb @@ -0,0 +1,166 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StripeAccount < ApplicationRecord + attr_accessible :object, :stripe_account_id + has_one :nonprofit, primary_key: :stripe_account_id + has_one :nonprofit_verification_process_status, primary_key: :stripe_account_id + + ## this scopes let you find accounts that do or do not have a future_requirements attribute + scope :with_future_requirements, -> {where("object->'future_requirements' IS NOT NULL") } + scope :without_future_requirements, -> {where("object->'future_requirements' IS NULL") } + + def object=(input) + serialize_on_update(input) + end + + def verification_status + if pending_verification.any? + result = :pending + elsif needs_immediate_validation_info + result = :unverified + elsif needs_more_validation_info + result = :temporarily_verified + else + result = :verified + end + result + end + + def requirements + Requirements.new(object['requirements']) + end + + def future_requirements + Requirements.new(object['future_requirements'] || {}) + end + + # the distinct union of the current pending_verification and the future pending_verification values + def pending_verification + (requirements.pending_verification + future_requirements.pending_verification).uniq + end + + + # describes a deadline where additional requirements are needed to be completed + # future_requirements can have a current_deadline and not have any additional requirements so + # we don't consider that a deadline here. + def deadline + deadlines = [] + if requirements.current_deadline + deadlines.push(requirements.current_deadline) + end + if future_requirements.current_deadline && future_requirements.any_requirements_other_than_external_account?( + include_eventually_due: true, include_pending_verification: true + ) + deadlines.push(future_requirements.current_deadline) + end + + deadlines.min + end + + # these are validation requirements which may come in the future but haven't yet + def needs_more_validation_info + requirements.any_requirements_other_than_external_account?(include_eventually_due: true) + end + + # these are validation requirements which must be done by a given deadline + def needs_immediate_validation_info + deadline || requirements.any_requirements_other_than_external_account? + end + + def retrieve_from_stripe + Stripe::Account.retrieve(stripe_account_id, {stripe_version: '2020-08-27'}) + end + + def update_from_stripe + update(object: retrieve_from_stripe) + end + + private + def serialize_on_update(input) + + object_json = nil + + case input + when Stripe::Account + write_attribute(:object, input.to_hash) + object_json = self.object + puts self.object + when String + write_attribute(:object, input) + object_json = self.object + end + self.charges_enabled = !!object_json['charges_enabled'] + self.payouts_enabled = !!object_json['payouts_enabled'] + requirements = Requirements.new( object_json['requirements']) + self.disabled_reason = requirements.disabled_reason + self.currently_due = requirements.currently_due + self.past_due = requirements.past_due + self.eventually_due = requirements.eventually_due + self.pending_verification = requirements.pending_verification + + unless self.stripe_account_id + self.stripe_account_id = object_json['id'] + end + + self.object + end + + + # describes the Stripe Account Requirements in a more pleasant way + class Requirements + def initialize(requirements) + @requirements = requirements || {} + end + + def current_deadline + if @requirements['current_deadline'] && @requirements['current_deadline'].to_i != 0 + Time.at(@requirements['current_deadline'].to_i) + else + nil + end + end + + def disabled_reason + @requirements['disabled_reason'] + end + + def currently_due + @requirements['currently_due'] || [] + end + + def past_due + @requirements['past_due'] || [] + end + + def eventually_due + @requirements['eventually_due'] || [] + end + + def any_requirements_other_than_external_account?(opts={}) + + defaults = { + include_eventually_due: false, + include_pending_verification: false, + } + + opts = defaults.merge(opts) + requirement_arrays = [past_due, currently_due] + if opts[:include_eventually_due] + requirement_arrays.push(eventually_due) + end + + if opts[:include_pending_verification] + requirement_arrays.push(pending_verification) + end + + requirement_arrays.any? do |i| + !i.none? && !i.all? do |j| + j.starts_with?('external_account') + end + end + end + + def pending_verification + @requirements['pending_verification'] || [] + end + end +end diff --git a/app/models/stripe_charge.rb b/app/models/stripe_charge.rb new file mode 100644 index 000000000..fb9f04f3b --- /dev/null +++ b/app/models/stripe_charge.rb @@ -0,0 +1,33 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StripeCharge < ApplicationRecord + attr_accessible :object, :stripe_charge_id + has_one :charge, primary_key: :stripe_charge_id, foreign_key: :stripe_charge_id + + def object=(input) + serialize_on_update(input) + end + + def stripe_object + Stripe::Util.convert_to_stripe_object(object) + end + + private + def serialize_on_update(input) + + object_json = nil + + case input + when Stripe::Charge + write_attribute(:object, input.to_hash) + object_json = self.object + when String + write_attribute(:object, input) + object_json = self.object + end + + + self.stripe_charge_id = object_json['id'] + + self.object + end +end diff --git a/app/models/stripe_dispute.rb b/app/models/stripe_dispute.rb new file mode 100644 index 000000000..7637bb4c3 --- /dev/null +++ b/app/models/stripe_dispute.rb @@ -0,0 +1,191 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StripeDispute < ApplicationRecord + + TERMINAL_DISPUTE_STATUSES = ['won', 'lost'] + + attr_accessible :object, :stripe_dispute_id + has_one :dispute, primary_key: :stripe_dispute_id, foreign_key: :stripe_dispute_id + has_one :charge, primary_key: :stripe_charge_id, foreign_key: :stripe_charge_id + after_save :fire_change_events + + def object=(input) + serialize_on_update(input) + end + + def balance_transactions_state + StripeDispute.calc_balance_transaction_state(balance_transactions) + end + + def funds_withdrawn_balance_transaction + balance_transactions.any? ? balance_transactions.sort_by{|i| i['created']}[0] : nil + end + + def funds_reinstated_balance_transaction + balance_transactions.count == 2 ? balance_transactions.sort_by{|i| i['created']}[1] : nil + end + + private + def serialize_on_update(input) + + object_json = nil + + case input + when Stripe::Dispute + write_attribute(:object, input.to_hash) + object_json = self.object + when String + write_attribute(:object, input) + object_json = self.object + end + + self.balance_transactions = JSON.generate(object_json['balance_transactions']) + + self.reason = object_json['reason'] + self.status = object_json['status'] + self.net_change = object_json['balance_transactions'].map{|i| i['net']}.sum + self.amount = object_json['amount'] + self.stripe_dispute_id = object_json['id'] + self.stripe_charge_id = object_json['charge'] + self.evidence_due_date = (object_json['evidence_details'] && object_json['evidence_details']['due_by']) ? + Time.at(object_json['evidence_details']['due_by']) : + nil + self.started_at = Time.at(object_json['created']) + + self.object + end + + def fire_change_events + if changed? && !dispute&.is_legacy + if changed_attributes["object"].nil? + dispute_created_event + end + if balance_transactions_changed? + + old_bt, _ = balance_transactions_change + old_state = StripeDispute.calc_balance_transaction_state(old_bt) + if old_state != balance_transactions_state + + if (old_state == :none) + if( balance_transactions_state == :funds_withdrawn) + dispute_funds_withdrawn_event + else + dispute_funds_withdrawn_event + dispute_funds_reinstated_event + end + elsif (old_state == :funds_withdrawn) + if (balance_transactions_state == :funds_reinstated) + dispute_funds_reinstated_event + else + raise RuntimeError("Dispute #{dispute.id} previously had a balance_transaction_state of #{old_state} but is now #{balance_transactions_state}. " + + "This shouldn't be possible.") + end + elsif (balance_transactions_state != :funds_reinstated) + raise RuntimeError("Dispute #{dispute.id} previously had a balance_transaction_state of #{old_state} but is now #{balance_transactions_state}. " + + "This shouldn't be possible.") + end + end + end + + if status_changed? + if TERMINAL_DISPUTE_STATUSES.include?(changed_attributes['status']) && !TERMINAL_DISPUTE_STATUSES.include?(status) + # if previous status was won or lost and the new one isn't + raise RuntimeError("Dispute #{dispute.id} was previously #{changed_attributes['status']} but is now #{status}. " + + "This shouldn't be possible") + elsif (!TERMINAL_DISPUTE_STATUSES.include?(changed_attributes['status']) && TERMINAL_DISPUTE_STATUSES.include?(status)) + # previous status was not won or lost but the new one is + + dispute_closed_event + else + if (changed_attributes["status"] != nil) + # previous status was not won or lost but the new one still isn't but there were changes! + dispute_updated_event + end + end + elsif (!balance_transactions_changed? && !changed_attributes["object"].nil?) + dispute_updated_event + end + end + end + + def dispute_created_event + create_dispute!(charge:charge, status:status, reason: reason, gross_amount:amount, started_at: started_at) + + # notify folks of the event being opened + dispute.activities.create('DisputeCreated', started_at) + JobQueue.queue(JobTypes::DisputeCreatedJob, dispute) + end + + def dispute_funds_withdrawn_event + gross_amount = funds_withdrawn_balance_transaction["amount"] + fee_total = -1 * funds_withdrawn_balance_transaction['fee'] + transaction = dispute.dispute_transactions.create(gross_amount:gross_amount, fee_total: fee_total , + payment: dispute.supporter.payments.create(nonprofit: dispute.nonprofit, + gross_amount:gross_amount, + fee_total: fee_total, + net_amount: gross_amount + fee_total, + kind: 'Dispute', + date: Time.at(funds_withdrawn_balance_transaction["created"]) + ), + stripe_transaction_id: funds_withdrawn_balance_transaction['id'], + date: Time.at(funds_withdrawn_balance_transaction["created"]) + ) + + # add dispute payment activity + transaction.payment.activities.create + + transaction.dispute.original_payment.refund_total += gross_amount * -1 + transaction.dispute.original_payment.save! + # notify folks of the withdrawal + JobQueue.queue(JobTypes::DisputeFundsWithdrawnJob, dispute) + end + + def dispute_funds_reinstated_event + gross_amount = funds_reinstated_balance_transaction["amount"] + fee_total = -1 * funds_reinstated_balance_transaction['fee'] + transaction = dispute.dispute_transactions.create(gross_amount:gross_amount, fee_total: fee_total, + payment: dispute.supporter.payments.create(nonprofit: dispute.nonprofit, + gross_amount:gross_amount, + fee_total: fee_total, + net_amount: gross_amount + fee_total, + kind: 'DisputeReversed', + date: Time.at(funds_reinstated_balance_transaction["created"]) + ), + stripe_transaction_id: funds_reinstated_balance_transaction['id'], + date: Time.at(funds_reinstated_balance_transaction["created"]) + ) + + transaction.dispute.original_payment.refund_total += gross_amount * -1 + transaction.dispute.original_payment.save! + # add dispute payment activity + transaction.payment.activities.create + JobQueue.queue(JobTypes::DisputeFundsReinstatedJob, dispute) + end + + def dispute_closed_event + dispute.status = status + dispute.save! + if (dispute.status == 'won') + dispute.activities.create('DisputeWon', Time.now) + JobQueue.queue(JobTypes::DisputeWonJob, dispute) + elsif dispute.status == 'lost' + dispute.activities.create('DisputeLost', Time.now) + JobQueue.queue(JobTypes::DisputeLostJob, dispute) + else + raise RuntimeError("Dispute #{dispute.id} was closed " + + "but had status of #{dispute.status}") + end + end + + def dispute_updated_event + dispute.activities.create('DisputeUpdated', Time.now) + JobQueue.queue(JobTypes::DisputeUpdatedJob, dispute) + end + + def self.calc_balance_transaction_state(balance_transactions) + !balance_transactions || balance_transactions.count == 0 ? + :none : + balance_transactions.count == 1 ? + :funds_withdrawn : + :funds_reinstated + end +end diff --git a/app/models/stripe_event.rb b/app/models/stripe_event.rb new file mode 100644 index 000000000..0915a1050 --- /dev/null +++ b/app/models/stripe_event.rb @@ -0,0 +1,147 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class StripeEvent < ApplicationRecord + attr_accessible :event_id, :event_time, :object_id + + def self.process_dispute(event) + StripeEvent.transaction do + object = event.data.object + events_for_object_id = StripeEvent.where('object_id = ?', object.id).lock(true) + + event_record = events_for_object_id.where('event_id = ?', event.id).first + + # if event_record found, we've recorded this event so no processing necessary + unless event_record + # we record this event! + stripe_event = StripeEvent.new(event_id: event.id, event_time: Time.at(event.created).to_datetime, object_id: event.data.object.id) + stripe_event.save! + + later_event = events_for_object_id.where('event_time > ?', Time.at(event.created).to_datetime).first + + # we have a later event so we don't need to process this anymore + unless later_event + LockManager.with_transaction_lock(object.id) do + object = Stripe::Dispute.retrieve(object.id) + dispute = StripeDispute.where("stripe_dispute_id = ?", object.id).first + unless dispute + dispute = StripeDispute.new(stripe_dispute_id: object.id) + end + dispute.object = object + dispute.save! + end + end + end + end + end + + def self.process_charge(event) + StripeEvent.transaction do + object = event.data.object + events_for_object_id = StripeEvent.where('object_id = ?', object.id).lock(true) + + event_record = events_for_object_id.where('event_id = ?', event.id).first + + # if event_record found, we've recorded this event so no processing necessary + unless event_record + # we record this event! + stripe_event = StripeEvent.new(event_id: event.id, event_time: Time.at(event.created).to_datetime, object_id: event.data.object.id) + stripe_event.save! + + later_event = events_for_object_id.where('event_time > ?', Time.at(event.created).to_datetime).first + + # we have a later event so we don't need to process this anymore + unless later_event + LockManager.with_transaction_lock(object.id) do + object = Stripe::Charge.retrieve(object.id) + charge = StripeCharge.where("stripe_charge_id = ?", object.id).first + unless charge + charge = StripeCharge.new(stripe_charge_id: object.id) + end + charge.object = object + charge.save! + end + end + end + end + end + + def self.handle(event) + case event.type + when 'account.updated' + StripeEvent.transaction do + object = event.data.object + events_for_object_id = StripeEvent.where('object_id = ?', object.id).lock(true) + + event_record = events_for_object_id.where('event_id = ?', event.id).first + + # if event_record found, we've recorded this event so no processing necessary + unless event_record + # we record this event! + stripe_event = StripeEvent.new(event_id: event.id, event_time: Time.at(event.created).to_datetime, object_id: event.data.object.id) + stripe_event.save! + + later_event = events_for_object_id.where('event_time > ?', Time.at(event.created).to_datetime).first + + # we have a later event so we don't need to process this anymore + unless later_event + previous_verification_status = nil + account = StripeAccount.where("stripe_account_id = ?", object.id).first + if account + account.lock!('FOR UPDATE') + previous_verification_status = account.verification_status + else + account = StripeAccount.new(stripe_account_id: object.id) + end + + status = NonprofitVerificationProcessStatus.where(stripe_account_id: object.id).first + + account.object = object + account.save! + + + unless status.nil? + if [:verified, :temporarily_verified].include?(account.verification_status) + status.destroy if status.persisted? + #send validation email + StripeAccountMailer.delay.conditionally_send_verified(account) + else + status.email_to_send_guid = SecureRandom.uuid + + if previous_verification_status == :pending + StripeAccountMailer.delay(run_at: DateTime.now + NONPROFIT_VERIFICATION_SEND_EMAIL_DELAY).conditionally_send_more_info_needed(account, status.email_to_send_guid) + else + StripeAccountMailer.delay(run_at: DateTime.now + NONPROFIT_VERIFICATION_SEND_EMAIL_DELAY).conditionally_send_not_completed(account, status.email_to_send_guid) + end + + status.save! + end + else + if previous_verification_status == :verified && account.verification_status == :unverified + StripeAccountMailer.delay.conditionally_send_no_longer_verified(account) + end + end + end + end + end + when 'charge.dispute.created' + process_dispute(event) + when 'charge.dispute.funds_withdrawn' + process_dispute(event) + when 'charge.dispute.funds_reinstated' + process_dispute(event) + when 'charge.dispute.closed' + process_dispute(event) + when 'charge.captured' + process_charge(event) + when 'charge.expired' + process_charge(event) + when 'charge.failed' + process_charge(event) + when 'charge.pending' + process_charge(event) + when 'charge.succeeded' + process_charge(event) + when 'charge.updated' + process_charge(event) + end + end +end diff --git a/app/models/stripe_transaction.rb b/app/models/stripe_transaction.rb new file mode 100644 index 000000000..a4a24c573 --- /dev/null +++ b/app/models/stripe_transaction.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class StripeTransaction < ApplicationRecord + include Model::Subtransactable + setup_houid :stripetrx, :houid + delegate :created, to: :subtransaction + delegate :net_amount, to: :subtransaction_payments + + as_money :amount, :net_amount + + # Handle a completed refund from a legacy Refund object + def process_refund(refund) + refund = self.subtransaction.subtransaction_payments.create!(paymentable:StripeTransactionRefund.new, subtransaction: subtransaction, legacy_payment: refund.payment) + update!(amount: subtransaction_payments.gross_amount) + refund + end + + def publish_created + #object_events.create( event_type: 'stripe_transaction.created') + end + + def publish_updated + #object_events.create( event_type: 'stripe_transaction.updated') + end + + def publish_deleted + #object_events.create( event_type: 'stripe_transaction.deleted') + end +end diff --git a/app/models/stripe_transaction_charge.rb b/app/models/stripe_transaction_charge.rb new file mode 100644 index 000000000..9cbc4d354 --- /dev/null +++ b/app/models/stripe_transaction_charge.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class StripeTransactionCharge < ApplicationRecord + include Model::SubtransactionPaymentable + setup_houid :stripechrg, :houid + + has_one :legacy_payment, class_name: 'Payment', through: :subtransaction_payment + + delegate :gross_amount, :net_amount, :fee_total, to: :legacy_payment + + as_money :gross_amount, :net_amount, :fee_total + + def created + legacy_payment.date + end + + def stripe_id + legacy_payment.charge.stripe_charge_id + end + + def publish_created + object_events.create( event_type: 'stripe_transaction_charge.created') + end + + def publish_updated + object_events.create( event_type: 'stripe_transaction_charge.updated') + end + + def publish_deleted + object_events.create( event_type: 'stripe_transaction_charge.deleted') + end +end diff --git a/app/models/stripe_transaction_dispute.rb b/app/models/stripe_transaction_dispute.rb new file mode 100644 index 000000000..1270a1f20 --- /dev/null +++ b/app/models/stripe_transaction_dispute.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class StripeTransactionDispute < ApplicationRecord + include Model::SubtransactionPaymentable + setup_houid :stripedisp, :houid + has_one :legacy_payment, class_name: 'Payment', through: :subtransaction_payment + + delegate :gross_amount, :net_amount, :fee_total, to: :legacy_payment + + as_money :gross_amount, :net_amount, :fee_total + + def created + legacy_payment.date + end + + # def stripe_id + # legacy_payment.dispute.stripe_dispute_id + # end + + def publish_created + object_events.create( event_type: 'stripe_transaction_dispute.created') + end + + def publish_updated + object_events.create( event_type: 'stripe_transaction_dispute.updated') + end + + def publish_deleted + object_events.create( event_type: 'stripe_transaction_dispute.deleted') + end +end diff --git a/app/models/stripe_transaction_dispute_reversal.rb b/app/models/stripe_transaction_dispute_reversal.rb new file mode 100644 index 000000000..3e47f5780 --- /dev/null +++ b/app/models/stripe_transaction_dispute_reversal.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class StripeTransactionDisputeReversal < ApplicationRecord + include Model::SubtransactionPaymentable + setup_houid :stripedisprvrs, :houid + + has_one :legacy_payment, class_name: 'Payment', through: :subtransaction_payment + + delegate :gross_amount, :net_amount, :fee_total, to: :legacy_payment + + delegate :currency, to: :nonprofit + + as_money :gross_amount, :net_amount, :fee_total + + def created + legacy_payment.date + end + + + def publish_created + object_events.create( event_type: 'stripe_transaction_dispute_reversal.created') + end + + def publish_updated + object_events.create( event_type: 'stripe_transaction_dispute_reversal.updated') + end + + def publish_deleted + object_events.create( event_type: 'stripe_transaction_dispute_reversal.deleted') + end +end \ No newline at end of file diff --git a/app/models/stripe_transaction_refund.rb b/app/models/stripe_transaction_refund.rb new file mode 100644 index 000000000..e17d6622f --- /dev/null +++ b/app/models/stripe_transaction_refund.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class StripeTransactionRefund < ApplicationRecord + include Model::SubtransactionPaymentable + setup_houid :striperef, :houid + + has_one :legacy_payment, class_name: 'Payment', through: :subtransaction_payment + + delegate :gross_amount, :net_amount, :fee_total, to: :legacy_payment + + as_money :gross_amount, :net_amount, :fee_total + + def created + legacy_payment.date + end + + + def stripe_id + legacy_payment.refund.stripe_refund_id + end + + def publish_created + object_events.create( event_type: 'stripe_transaction_refund.created') + end + + def publish_updated + object_events.create( event_type: 'stripe_transaction_refund.updated') + end + + def publish_deleted + object_events.create( event_type: 'stripe_transaction_refund.deleted') + end + +end \ No newline at end of file diff --git a/app/models/subtransaction.rb b/app/models/subtransaction.rb new file mode 100644 index 000000000..ba10a378d --- /dev/null +++ b/app/models/subtransaction.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class Subtransaction < ApplicationRecord + include Model::CreatedTimeable + + belongs_to :trx, class_name: 'Transaction', foreign_key: 'transaction_id', inverse_of: :subtransaction + has_one :supporter, through: :trx + has_one :nonprofit, through: :trx + delegate :currency, to: :nonprofit + + belongs_to :subtransactable, polymorphic: true + + has_many :subtransaction_payments, -> { extending ModelExtensions::PaymentsExtension } # rubocop:disable Rails/HasManyOrHasOneDependent + + # get payments in reverse chronological order + def ordered_payments + subtransaction_payments.ordered + end + + delegated_type :subtransactable, types: %w[OfflineTransaction, StripeTransaction] + + delegate :to_houid, :process_refund, :publish_updated, to: :subtransactable + + as_money :amount + + validates_presence_of :subtransactable +end diff --git a/app/models/subtransaction_payment.rb b/app/models/subtransaction_payment.rb new file mode 100644 index 000000000..2fab99937 --- /dev/null +++ b/app/models/subtransaction_payment.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class SubtransactionPayment < ApplicationRecord + include Model::CreatedTimeable + + + # get payments in reverse chronological order using SQL + scope :ordered_query, -> { + includes(:legacy_payment).references(:legacy_payments).order("payments.date DESC").order("subtransaction_payments.updated_at DESC") + } + + belongs_to :subtransaction, inverse_of: :subtransaction_payments + has_one :trx, class_name: 'Transaction', foreign_key: 'transaction_id', through: :subtransaction + has_one :supporter, through: :subtransaction + has_one :nonprofit, through: :subtransaction + belongs_to :legacy_payment, class_name: 'Payment', required: true + + delegated_type :paymentable, types: %w[ + OfflineTransactionCharge + OfflineTransactionDispute + OfflineTransactionRefund + StripeTransactionCharge + StripeTransactionRefund + StripeTransactionDispute + StripeTransactionDisputeReversal + ] + + delegate :gross_amount, :fee_total, :net_amount, :publish_created, :publish_updated, :publish_deleted, :to_houid, to: :paymentable + + validates_presence_of :paymentable +end diff --git a/app/models/supporter.rb b/app/models/supporter.rb index 8d5a89dee..fc07861f9 100644 --- a/app/models/supporter.rb +++ b/app/models/supporter.rb @@ -1,8 +1,19 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class Supporter < ActiveRecord::Base +class Supporter < ApplicationRecord + include Model::Houidable + include Model::CalculatedNames + setup_houid :supp, :houid + ADDRESS_FIELDS = ['address', 'city', 'state_code', 'country', 'zip_code'] + + before_validation :cleanup_address + before_validation :cleanup_name + + before_save :update_primary_address + + attr_accessor :address_line2 + attr_accessible \ - :search_vectors, :profile_id, :profile, :nonprofit_id, :nonprofit, :full_contact_info, :full_contact_info_id, @@ -27,15 +38,33 @@ class Supporter < ActiveRecord::Base :anonymous, :deleted, # bool (flag for soft delete) :email_unsubscribe_uuid, #string - :is_unsubscribed_from_emails #bool + :is_unsubscribed_from_emails, #bool + :id, + :created_at, + :address_line2, + :primary_address + + # fts is generated via a trigger + attr_readonly :fts belongs_to :profile belongs_to :nonprofit belongs_to :import has_many :full_contact_infos - has_many :payments + has_many :payments do + def during_np_year(year) + proxy_association.owner.nonprofit.use_zone do + where('date >= ? and date < ?', Time.zone.local(year), Time.zone.local(year + 1)) + end + end + end has_many :offsite_payments + has_many :charges + has_many :refunds, through: :charges + has_many :disputes, through: :charges + has_many :transactions + has_many :cards, as: :holder has_many :direct_debit_details has_many :donations @@ -44,14 +73,65 @@ class Supporter < ActiveRecord::Base has_many :activities, dependent: :destroy has_many :tickets has_many :recurring_donations - has_many :tag_joins, dependent: :destroy - has_many :tag_masters, through: :tag_joins + has_many :object_events, as: :event_entity + + concerning :Tags do + included do + has_many :tag_joins, dependent: :destroy + has_many :tag_masters, through: :tag_joins + has_many :undeleted_tag_masters, -> { not_deleted }, through: :tag_joins, source: 'tag_master' + end + end + + concerning :EmailLists do + include Supporter::Tags # not needed but helpful for tracking dependencies + included do + has_many :email_lists, through: :tag_masters + has_many :active_email_lists, through: :undeleted_tag_masters, source: :email_list do + def update_member_on_all_lists + proxy_association.reload.target.each do |list| # We're reloading the association and running .each on target + #to make sure we get any newly saved email lists. I think this should be simpler but I'm not sure how to do it. + MailchimpSignupJob.perform_later(proxy_association.owner, list) + end + end + end + + after_save :try_update_member_on_all_lists + end + + def must_update_email_lists? + changes.has_key?("name") || changes.has_key?("email") + end + + def publish_created + object_events.create(event_type: 'supporter.created') + end + + private + + def try_update_member_on_all_lists + update_member_on_all_lists if must_update_email_lists? + end + + def update_member_on_all_lists + active_email_lists.update_member_on_all_lists + end + + end + 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' + has_many :merged_from, class_name: 'Supporter', :foreign_key => "merged_into" + + has_many :addresses, class_name: "SupporterAddress", after_add: :set_address_to_primary_if_needed + belongs_to :primary_address, class_name: "SupporterAddress" validates :nonprofit, :presence => true scope :not_deleted, -> {where(deleted: false)} + scope :deleted, -> {where(deleted: true)} + scope :merged, -> {where('merged_at IS NOT NULL')} + scope :not_merged, -> {where('merged_at IS NULL')} geocoded_by :full_address reverse_geocoded_by :latitude, :longitude do |obj, results| @@ -70,6 +150,33 @@ def profile_picture size=:normal self.profile.get_profile_picture(size) end + concerning :Path do + class_methods do + ModernParams = Struct.new(:to_param) + end + included do + # When you use a routing helper like `api_new_nonprofit_supporter``, you need to provide objects which have a `#to_param` + # method. By default that's set to the value of `#id`. In our case, for the api objects, we want the id to instead be + # the value of `#houid`. We can't override `to_param` though because we may use route helpers which expect `#to_param` to + # return the value of `#id`. This is the hacky workaround. + def to_modern_param + ModernParams.new(houid) + end + end + end + + + + # Supporters can be merged many times. This finds the last + # supporter after following merged_into until it gets a nil + def end_of_merge_chain + if merged_into.nil? + return self + else + merged_into.end_of_merge_chain + end + end + def as_json(options = {}) h = super(options) @@ -83,4 +190,57 @@ def full_address Format::Address.full_address(self.address, self.city, self.state_code) end + private + def cleanup_address + if address.present? && address_line2.present? + assign_attributes(address_line2: nil, address: self.address + " " + self.address_line2) + end + address_field_attributes.each do |addr_attribute, addr_value| + self[addr_attribute] = nil if addr_value.blank? + end + end + + def cleanup_name + if first_name.present? || last_name.present? + assign_attributes(name: [first_name&.strip, last_name&.strip].select{|i| i.present?}.join(" ")) + assign_attributes(first_name: nil, last_name: nil) + end + end + + def address_field_attributes + attributes.slice(*ADDRESS_FIELDS) + end + + def filled_address_fields? + address_field_attributes.any? { |column, value| value.present? } + end + + def update_primary_address + if self.changes.slice(*ADDRESS_FIELDS).any? #changed an address field + if filled_address_fields? + if primary_address.nil? + self.addresses.build(address_field_attributes) + else + primary_address.update(address_field_attributes) + end + elsif primary_address.present? + prim_addr = primary_address + self.update(primary_address: nil) + self.addresses.delete(prim_addr) + prim_addr.destroy + end + end + end + + def set_address_to_primary_if_needed(new_address) + if primary_address.nil? + assign_attributes(primary_address: new_address) + end + end + + concerning :Mailchimp do + def md5_hash_of_email + Digest::MD5.hexdigest email.downcase + end + end end diff --git a/app/models/supporter_address.rb b/app/models/supporter_address.rb new file mode 100644 index 000000000..6bfa715b1 --- /dev/null +++ b/app/models/supporter_address.rb @@ -0,0 +1,8 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class SupporterAddress < ApplicationRecord + belongs_to :supporter, required:true, inverse_of: :addresses + + def primary? + supporter&.primary_address == self + end +end diff --git a/app/models/supporter_email.rb b/app/models/supporter_email.rb index 5ac0d8d98..0e9b2e530 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 dd920f077..7b6da4c4f 100644 --- a/app/models/supporter_note.rb +++ b/app/models/supporter_note.rb @@ -1,14 +1,29 @@ # 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, - :supporter_id, :supporter + :supporter_id, :supporter, + :user belongs_to :supporter has_many :activities, as: :attachment, dependent: :destroy + belongs_to :user validates :content, length: {minimum: 1} - validates :supporter_id, presence: true + validates :supporter, presence: true + + after_create :create_activity + + concerning :ETapImport do + included do + has_many :journal_entries_to_items, as: :item + end + end + + private + def create_activity + InsertActivities.for_supporter_notes([id]) + end end diff --git a/app/models/tag_join.rb b/app/models/tag_join.rb index 5f0e402ae..d531ddb93 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_join/modification.rb b/app/models/tag_join/modification.rb new file mode 100644 index 000000000..efea503cc --- /dev/null +++ b/app/models/tag_join/modification.rb @@ -0,0 +1,34 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# Describes a single requested modification to the list of tags on a supporter +class TagJoin::Modification + include ActiveModel::AttributeAssignment + attr_reader :tag_master_id, :selected + + def initialize(opts={}) + # TODO move the parameters further out + assign_attributes(ActionController::Parameters.new(opts).permit(:tag_master_id, :selected)) + end + + def tag_master_id=(value) + @tag_master_id = cast_integer(value) + end + + def selected=(value) + @selected = cast_boolean(value) + end + + def tag_master + TagMaster.find(tag_master_id) + end + + + private + + def cast_boolean(value) + ActiveRecord::Type::Boolean.new.type_cast_from_user(value) + end + + def cast_integer(value) + ActiveRecord::Type::Integer.new.type_cast_from_user(value) + end +end \ No newline at end of file diff --git a/app/models/tag_join/modifications.rb b/app/models/tag_join/modifications.rb new file mode 100644 index 000000000..6a2c368c0 --- /dev/null +++ b/app/models/tag_join/modifications.rb @@ -0,0 +1,52 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +# Handy class managing a list of requested modifications +class TagJoin::Modifications < Array + + # accepts an Array containing hash with the keys `tag_master_id`` and `selected` or `TagJoin::Modification`'s + def initialize(tag_modifications_source=[]) + super(tag_modifications_source.map do |i| + i.is_a?(TagJoin::Modification) ? i : TagJoin::Modification.new(i) + end + ) + end + + # @return [TagJoin::Modifications] all tags which are selected + def selected + TagJoin::Modifications.new(self.select{|i| i.selected}) + end + + # @return [TagJoin::Modifications] all tags which are not selected + def unselected + TagJoin::Modifications.new(self.select{|i| !i.selected}) + end + + def to_tag_master_ids + self.map{|i| i.tag_master_id} + end + + # given a set of ids for TagMaster OR TagMaster objects themeselves, + # returns a TagJoin::Modifications with the TagJoin::Modification's which + # have a corresponding id + # @param [Array] tags a list of TagMaster or ids for TagMasters to match against + # @return [TagJoin::Modification] TagJoin::Modification with all modifications which matches the passed in list + # @example passing in a TagMaster + # given_tag_master = TagMaster.find(1234) + # modifications = TagJoin::Modifications.new([{tag_master_id: 5678, selected: true}, {tag_master_id: 1234, selected: false}]) + # + # mods_for_tags = modifications.for_given_tags([given_tag_master]) + # # => [#] + # + # + def for_given_tags(tags=[]) + valid_ids = tags.map do |i| + if (i.is_a? Integer) + i + else + i.id + end + end + + TagJoin::Modifications.new(self.select{|i| valid_ids.include? i.tag_master_id}) + end + +end \ No newline at end of file diff --git a/app/models/tag_master.rb b/app/models/tag_master.rb index 6e9f0ffd4..836bdbfb4 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 e805ad897..28bd0bcee 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 @@ -12,6 +12,7 @@ class Ticket < ActiveRecord::Base belongs_to :card belongs_to :payment belongs_to :source_token + belongs_to :ticket_purchase has_one :nonprofit, through: :event has_many :activities, as: :attachment, dependent: :destroy diff --git a/app/models/ticket_level.rb b/app/models/ticket_level.rb index 9d628782f..130157e0a 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/ticket_purchase.rb b/app/models/ticket_purchase.rb new file mode 100644 index 000000000..1c4e7f5a6 --- /dev/null +++ b/app/models/ticket_purchase.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +class TicketPurchase < ApplicationRecord + include Model::TrxAssignable + + setup_houid :tktpur, :houid + + has_many :tickets +end diff --git a/app/models/tracking.rb b/app/models/tracking.rb index d3a175c77..a730989f2 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/transaction.rb b/app/models/transaction.rb new file mode 100644 index 000000000..33904335b --- /dev/null +++ b/app/models/transaction.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class Transaction < ApplicationRecord + include Model::CreatedTimeable + include Model::Houidable + + setup_houid :trx, :houid + + belongs_to :supporter + has_one :nonprofit, through: :supporter + + has_many :transaction_assignments, -> { extending ModelExtensions::TransactionAssignment::RefundExtension }, inverse_of: 'trx' + has_many :donations, through: :transaction_assignments, source: :assignable, source_type: 'ModernDonation', inverse_of: 'trx' + has_many :ticket_purchases, through: :transaction_assignments, source: :assignable, source_type: 'TicketPurchase', inverse_of: 'trx' + + has_one :subtransaction, inverse_of: :trx + has_many :payments, -> { extending ModelExtensions::PaymentsExtension }, through: :subtransaction, source: :subtransaction_payments, class_name: 'SubtransactionPayment' + + has_many :object_events, as: :event_entity + + validates :supporter, presence: true + + delegate :currency, to: :nonprofit + + as_money :amount + + # get payments in reverse chronological order + def ordered_payments + payments.ordered + end + + # def designation + # donation&.designation + # end + + # def dedication + # donation&.dedication + # end + + concerning :Refunds do + # Handle a completed refund from a legacy Refund object + def process_refund(refund) + new_refund = save_refund(refund) + publish_after_refund(new_refund) + end + + private + + # @param refund Refund a refund object + # @returns StripeTransactionPayment (with a StripeTransactionRefund) represents the new refund + def save_refund(refund) + # add the refund to the subtransaction as a StripeTransactionRefund + new_refund = subtransaction.process_refund(refund) + # update the value of the transaction itself from the subtransaction + self.amount = subtransaction.subtransactable.amount + # refund means we need to adjust the values of the transaction_assignments + transaction_assignments.process_refund(refund) + # save everything + save! + + new_refund + end + + # @param refund StripeTransactionPayment (with a StripeTransactionRefund) represents the new refund + def publish_after_refund(new_refund) + publish_updated + + subtransaction.publish_updated + # we want to publish that every payment has other than the new refund been updated + payments.ordered.select{|i| i != new_refund}.each(&:publish_updated) + # publish that the new_refund has been created + new_refund.publish_created + + # publish that the transaction_assignments have been updated + transaction_assignments.first.publish_updated + end + end + + def publish_created + object_events.create( event_type: 'transaction.created') + end + + def publish_updated + object_events.create( event_type: 'transaction.updated') + end + + def publish_deleted + object_events.create( event_type: 'transaction.deleted') + end + + private + + def to_param + persisted? && houid + end +end + + +ActiveSupport.run_load_hooks(:houdini_transaction, Transaction) diff --git a/app/models/transaction_assignment.rb b/app/models/transaction_assignment.rb new file mode 100644 index 000000000..62ca9af0b --- /dev/null +++ b/app/models/transaction_assignment.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class TransactionAssignment < ApplicationRecord + + delegated_type :assignable, types: ['ModernDonation'] + + belongs_to :trx, class_name: 'Transaction', foreign_key: "transaction_id", required:true, inverse_of: :transaction_assignments + has_one :supporter, through: :trx + has_one :nonprofit, through: :trx + + validates_presence_of :assignable + + delegate :to_houid, :publish_updated, to: :assignable + +end diff --git a/app/models/user.rb b/app/models/user.rb index 32f1e6743..e61d2afce 100755 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -1,5 +1,6 @@ # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -class User < ActiveRecord::Base +class User < ApplicationRecord + include Model::CalculatedNames attr_accessible \ :email, # str: balidated with Devise @@ -27,10 +28,15 @@ class User < ActiveRecord::Base geocoded_by :location - devise :async, :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable + devise :database_authenticatable, :registerable, :confirmable, :recoverable, :rememberable, :trackable, :validatable, + :lockable attr_accessor :offsite_donation_id, :current_password + scope :nonprofit_admins, -> { includes(:roles).where("roles.name = 'nonprofit_admin'").references(:roles) } + scope :nonprofit_associates, -> { includes(:roles).where("roles.name = 'nonprofit_associate'").references(:roles) } + scope :nonprofit_personnel, -> {includes(:roles).where("roles.name = 'nonprofit_admin' OR roles.name='nonprofit_associate' ").references(:roles) } + validates :email, presence: true, uniqueness: {case_sensitive: false}, @@ -41,6 +47,7 @@ class User < ActiveRecord::Base has_one :profile, dependent: :destroy has_many :imports has_many :email_settings + has_and_belongs_to_many :periodic_reports accepts_nested_attributes_for :profile @@ -84,6 +91,11 @@ def confirmation_required? false end + # This lists the nonprofit_admin rules for the given user. There should only be one. + def nonprofit_admin_roles + roles.where(host_type: "Nonprofit").nonprofit_admins + end + def as_json(options={}) h = super(options) h[:unconfirmed_email] = self.unconfirmed_email @@ -100,6 +112,25 @@ def make_confirmation_token! self.save! return raw end + + # override the main devise_notification code because we're using Delayed::Job + def send_devise_notification(notification, *args) + message = devise_mailer.delay.send(notification, self, *args) + end + + # override devise class method send_reset_password_instructions to limit to 1 request / 5 min + def self.send_reset_password_instructions(attributes={}) + recoverable = find_or_initialize_with_errors(reset_password_keys, attributes, :not_found) + if recoverable.persisted? + if recoverable.reset_password_sent_at.nil? || Time.now > recoverable.reset_password_sent_at + 5.minutes + recoverable.send_reset_password_instructions + return recoverable + else + recoverable.errors.add(:base, "can't reset password because a request was just sent") + end + end + recoverable + end def geocode! #self.geocode diff --git a/app/models/widget_description.rb b/app/models/widget_description.rb new file mode 100644 index 000000000..a8740d6c1 --- /dev/null +++ b/app/models/widget_description.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +class WidgetDescription < ApplicationRecord + setup_houid :wdgtdesc, :houid + + has_many :campaigns + + validate :is_postfix_element_a_hash, :is_postfix_element_correct + + + validates_length_of :custom_amounts, minimum: 1, allow_nil:true + + validate :are_custom_amounts_correct + + def to_json_safe_keys + attributes.slice('custom_amounts', 'postfix_element', 'custom_recurring_donation_phrase') + end + + private + + def are_custom_amounts_correct + unless custom_amounts.nil? + custom_amounts.each_with_index do |amount, index| + if amount.is_a? Hash + unless (amount.has_key?('amount') && amount['amount'].is_a?(Integer)) + errors.add(:custom_amounts, "has an invalid amount #{amount} at index #{index}") + end + + elsif !amount.is_a? Integer + errors.add(:custom_amounts, "has an invalid amount #{amount} at index #{index}") + end + end + end + end + + + def is_postfix_element_a_hash + errors.add(:postfix_element, "must be a hash or nil") unless postfix_element.nil? || postfix_element.is_a?(Hash) + end + + def is_postfix_element_correct + if postfix_element.is_a? Hash + if !postfix_element.has_key?('type') || postfix_element['type'] != 'info' || !postfix_element.has_key?('html_content') + errors.add(:postfix_element, "has invalid contents") + end + end + end +end diff --git a/app/validators/postgresql_date_format_validator.rb b/app/validators/postgresql_date_format_validator.rb new file mode 100644 index 000000000..33b3ce790 --- /dev/null +++ b/app/validators/postgresql_date_format_validator.rb @@ -0,0 +1,34 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +class PostgresqlDateFormatValidator < ActiveModel::Validator + # based on https://www.postgresql.org/docs/current/functions-formatting.html + # must receive a { options: { attribute_name: } } to do the validation + def validate(record) + date_format = record[options[:attribute_name]] + unless (date_format.nil? || valid_date_format?(date_format)) + record.errors.add(:date_format, 'invalid date format') + end + end + + private + + def valid_date_format?(date_format) + ALLOWED_SEPARATORS.each do |separator| + date_format = (date_format.split(separator)).flatten + end + date_format.each do |date_pattern_element| + return false unless ALLOWED_POSTGRES_PATTERNS.include?(date_pattern_element) + end + true + end + + ALLOWED_SEPARATORS = ['/', '-', '.', ':'].freeze + + ALLOWED_POSTGRES_PATTERNS = [ + 'HH', 'HH12', 'HH24', 'MI', 'SS', 'MS', 'US', 'FF1', 'FF2', 'FF3', 'FF4', 'FF5', 'FF6', + 'SSSS', 'SSSSS', 'AM', 'am', 'PM', 'pm', 'A.M.', 'a.m.', 'P.M.', 'p.m.', 'Y', 'YYYY', 'YYY', + 'YY', 'Y', 'IYYY', 'IYY', 'IY', 'I', 'BC', 'bc', 'AD', 'ad', 'B.C.', 'b.c.', 'A.D.', 'a.d.', + 'MONTH', 'Month', 'month', 'MON', 'Mon', 'mon', 'MM', 'DAY', 'Day', 'day', 'DY', 'Dy', 'dy', + 'DDD', 'IDDD', 'DD', 'D', 'ID', 'W', 'WW', 'IW', 'CC', 'J', 'Q', 'RM', 'rm', 'TZ', 'tz', + 'TZH', 'TZM', 'OF' + ].freeze +end diff --git a/app/views/admin_mailer/notify_failed_gift.html.erb b/app/views/admin_mailer/notify_failed_gift.html.erb index 4d812ea26..cc9a6fe6b 100644 --- a/app/views/admin_mailer/notify_failed_gift.html.erb +++ b/app/views/admin_mailer/notify_failed_gift.html.erb @@ -4,7 +4,7 @@

The following donation was tried to be associated with a campaign gift option. You SHOULD refund the donation as there are no other gifts available:

-<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %> +<%= render 'donation_mailer/donation_payment_table', donation: @donation, payment: @payment %>

The associated gift was for:

diff --git a/app/views/api_new/common/_amount.json.jbuilder b/app/views/api_new/common/_amount.json.jbuilder new file mode 100644 index 000000000..0420ea824 --- /dev/null +++ b/app/views/api_new/common/_amount.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.(amount, :cents, :currency) diff --git a/app/views/api_new/errors/unauthorized.json.jbuilder b/app/views/api_new/errors/unauthorized.json.jbuilder new file mode 100644 index 000000000..4d1c29603 --- /dev/null +++ b/app/views/api_new/errors/unauthorized.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.extract! @error, :message diff --git a/app/views/api_new/modern_donations/_modern_donation.json.jbuilder b/app/views/api_new/modern_donations/_modern_donation.json.jbuilder new file mode 100644 index 000000000..9356af24a --- /dev/null +++ b/app/views/api_new/modern_donations/_modern_donation.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'donation' + + +json.(assignable, :designation, :legacy_id, :dedication, :comment) + +json.amount do + json.partial! '/api_new/common/amount', amount: assignable.amount_as_money +end diff --git a/app/views/api_new/modern_donations/object_events/_base.json.jbuilder b/app/views/api_new/modern_donations/object_events/_base.json.jbuilder new file mode 100644 index 000000000..4c5f983f1 --- /dev/null +++ b/app/views/api_new/modern_donations/object_events/_base.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/transaction_assignments/transaction_assignment', + transaction_assignment: event_entity.transaction_assignment, + __expand: build_json_expansion_path_tree( + 'transaction', + 'transaction.transaction_assignments', + 'transaction.subtransaction.payments' + ) \ No newline at end of file diff --git a/app/views/api_new/object_events/_object_event.json.jbuilder b/app/views/api_new/object_events/_object_event.json.jbuilder new file mode 100644 index 000000000..9d0ba3c5c --- /dev/null +++ b/app/views/api_new/object_events/_object_event.json.jbuilder @@ -0,0 +1,15 @@ + +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.id object_event.houid +json.created object_event.created.to_i +json.object 'object_event' +json.type object_event.event_type +json.data do + json.object do + json.partial! partial_path, event_entity: object_event.event_entity + end +end \ No newline at end of file diff --git a/app/views/api_new/object_events/generate.json.jbuilder b/app/views/api_new/object_events/generate.json.jbuilder new file mode 100644 index 000000000..46fc6ca1e --- /dev/null +++ b/app/views/api_new/object_events/generate.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.partial! @object_event, as: :object_event, object_as: @object_as, partial_path: @partial_path diff --git a/app/views/api_new/object_events/index.json.jbuilder b/app/views/api_new/object_events/index.json.jbuilder new file mode 100644 index 000000000..3fb0e5ebe --- /dev/null +++ b/app/views/api_new/object_events/index.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.data do + json.array! @object_events.map(&:object_json) +end + +json.current_page @object_events.current_page +json.first_page @object_events.first_page? +json.last_page@object_events.last_page? +json.requested_size @object_events.limit_value +json.total_count @object_events.total_count \ No newline at end of file diff --git a/app/views/api_new/offline_transaction_charges/_offline_transaction_charge.json.jbuilder b/app/views/api_new/offline_transaction_charges/_offline_transaction_charge.json.jbuilder new file mode 100644 index 000000000..b68b55bbe --- /dev/null +++ b/app/views/api_new/offline_transaction_charges/_offline_transaction_charge.json.jbuilder @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'offline_transaction_charge' + +json.created paymentable.created.to_i + +json.kind paymentable.subtransaction_payment.legacy_payment.offsite_payment&.kind +json.check_number paymentable.subtransaction_payment.legacy_payment.offsite_payment&.check_number + +json.net_amount do + json.partial! '/api_new/common/amount', amount: paymentable.net_amount_as_money +end + +json.gross_amount do + json.partial! '/api_new/common/amount', amount: paymentable.gross_amount_as_money +end + +json.fee_total do + json.partial! '/api_new/common/amount', amount: paymentable.fee_total_as_money +end diff --git a/app/views/api_new/offline_transaction_charges/object_events/_base.json.jbuilder b/app/views/api_new/offline_transaction_charges/object_events/_base.json.jbuilder new file mode 100644 index 000000000..bc2e74153 --- /dev/null +++ b/app/views/api_new/offline_transaction_charges/object_events/_base.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/subtransaction_payments/subtransaction_payment', + subtransaction_payment: event_entity.subtransaction_payment, + __expand: build_json_expansion_path_tree( + 'supporter', + 'subtransaction', + 'subtransaction.transaction', + 'subtransaction.transaction.transaction_assignments' + ) diff --git a/app/views/api_new/offline_transaction_charges/show.json.jbuilder b/app/views/api_new/offline_transaction_charges/show.json.jbuilder new file mode 100644 index 000000000..ed2bfc958 --- /dev/null +++ b/app/views/api_new/offline_transaction_charges/show.json.jbuilder @@ -0,0 +1,4 @@ +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! '/api_new/offline_transaction_charges/offline_transaction_charge', paymentable: @offline_transaction_charge diff --git a/app/views/api_new/offline_transactions/_offline_transaction.json.jbuilder b/app/views/api_new/offline_transactions/_offline_transaction.json.jbuilder new file mode 100644 index 000000000..0751c12d4 --- /dev/null +++ b/app/views/api_new/offline_transactions/_offline_transaction.json.jbuilder @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'offline_transaction' + +json.created subtransactable.created.to_i + +json.amount do + json.partial! '/api_new/common/amount', amount: subtransactable.amount_as_money +end + +json.net_amount do + json.partial! '/api_new/common/amount', amount: subtransactable.net_amount_as_money +end diff --git a/app/views/api_new/payouts/_payout.json.jbuilder b/app/views/api_new/payouts/_payout.json.jbuilder new file mode 100644 index 000000000..564719b52 --- /dev/null +++ b/app/views/api_new/payouts/_payout.json.jbuilder @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.id payout.houid + +json.object 'payout' + +json.created payout.created_at.to_i + +json.net_amount do + json.partial! '/api_new/common/amount', amount: payout.net_amount_as_money +end + +json.status payout.status diff --git a/app/views/api_new/payouts/object_events/_base.json.jbuilder b/app/views/api_new/payouts/object_events/_base.json.jbuilder new file mode 100644 index 000000000..5ade46412 --- /dev/null +++ b/app/views/api_new/payouts/object_events/_base.json.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/payouts/payout', + payout: event_entity diff --git a/app/views/api_new/payouts/show.json.jbuilder b/app/views/api_new/payouts/show.json.jbuilder new file mode 100644 index 000000000..d00e3d77b --- /dev/null +++ b/app/views/api_new/payouts/show.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! '/api_new/payouts/payout', payout: @payout \ No newline at end of file diff --git a/app/views/api_new/simple_objects/_simple_object.json.jbuilder b/app/views/api_new/simple_objects/_simple_object.json.jbuilder new file mode 100644 index 000000000..904382ae3 --- /dev/null +++ b/app/views/api_new/simple_objects/_simple_object.json.jbuilder @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.id object.houid + +json.object 'simple_object' + +handle_expansion(:parent, object.parent, {json: json, as: :object, __expand: __expand}) + +handle_expansion(:nonprofit, object.nonprofit, {json: json, __expand: __expand}) + +handle_array_expansion(:friends, object.friends, {json: json, item_as: :object, __expand: __expand}) do |expansion| + expansion.handle_item_expansion(expansion.item) +end + +handle_array_expansion(:friends_without_explicit_call, object.friends, {json: json, item_as: :object, __expand: __expand}) do |expansion| + expansion.handle_item_expansion +end + +handle_array_expansion(:friends_no_block_given, object.friends, {json: json, item_as: :object, __expand: __expand}) diff --git a/app/views/api_new/simple_objects/object_events/_base.json.jbuilder b/app/views/api_new/simple_objects/object_events/_base.json.jbuilder new file mode 100644 index 000000000..2e006313b --- /dev/null +++ b/app/views/api_new/simple_objects/object_events/_base.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! event_entity, as: :object, __expand: build_json_expansion_path_tree('parent') \ No newline at end of file diff --git a/app/views/api_new/simple_objects/show.json.jbuilder b/app/views/api_new/simple_objects/show.json.jbuilder new file mode 100644 index 000000000..85877155c --- /dev/null +++ b/app/views/api_new/simple_objects/show.json.jbuilder @@ -0,0 +1 @@ +json.partial! @simple_object, as: :object, __expand: @__expand \ No newline at end of file diff --git a/app/views/api_new/stripe_transaction_charges/_stripe_transaction_charge.json.jbuilder b/app/views/api_new/stripe_transaction_charges/_stripe_transaction_charge.json.jbuilder new file mode 100644 index 000000000..4c11ebfd5 --- /dev/null +++ b/app/views/api_new/stripe_transaction_charges/_stripe_transaction_charge.json.jbuilder @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'stripe_transaction_charge' + +json.net_amount do + json.partial! '/api_new/common/amount', amount: paymentable.net_amount_as_money +end + +json.gross_amount do + json.partial! '/api_new/common/amount', amount: paymentable.gross_amount_as_money +end + +json.fee_total do + json.partial! '/api_new/common/amount', amount: paymentable.fee_total_as_money +end diff --git a/app/views/api_new/stripe_transaction_charges/object_events/_base.json.jbuilder b/app/views/api_new/stripe_transaction_charges/object_events/_base.json.jbuilder new file mode 100644 index 000000000..106811ee3 --- /dev/null +++ b/app/views/api_new/stripe_transaction_charges/object_events/_base.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/subtransaction_payments/subtransaction_payment', + subtransaction_payment: event_entity.subtransaction_payment, + __expand: build_json_expansion_path_tree( + 'supporter', + 'subtransaction.payments', + 'subtransaction.transaction.transaction_assignments' + ) \ No newline at end of file diff --git a/app/views/api_new/stripe_transaction_dispute_reversals/_stripe_transaction_dispute_reversal.json.jbuilder b/app/views/api_new/stripe_transaction_dispute_reversals/_stripe_transaction_dispute_reversal.json.jbuilder new file mode 100644 index 000000000..0f51e753c --- /dev/null +++ b/app/views/api_new/stripe_transaction_dispute_reversals/_stripe_transaction_dispute_reversal.json.jbuilder @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'stripe_transaction_dispute_reversal' + +json.net_amount do + json.partial! '/api_new/common/amount', amount: paymentable.net_amount_as_money +end + +json.gross_amount do + json.partial! '/api_new/common/amount', amount: paymentable.gross_amount_as_money +end + +json.fee_total do + json.partial! '/api_new/common/amount', amount: paymentable.fee_total_as_money +end diff --git a/app/views/api_new/stripe_transaction_dispute_reversals/object_events/_base.json.jbuilder b/app/views/api_new/stripe_transaction_dispute_reversals/object_events/_base.json.jbuilder new file mode 100644 index 000000000..f123bb942 --- /dev/null +++ b/app/views/api_new/stripe_transaction_dispute_reversals/object_events/_base.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/subtransaction_payments/subtransaction_payment', + subtransaction_payment: event_entity.subtransaction_payment, + __expand: build_json_expansion_path_tree( + 'supporter', + 'subtransaction', + 'subtransaction.transaction', + 'subtransaction.transaction.transaction_assignments' + ) \ No newline at end of file diff --git a/app/views/api_new/stripe_transaction_disputes/_stripe_transaction_dispute.json.jbuilder b/app/views/api_new/stripe_transaction_disputes/_stripe_transaction_dispute.json.jbuilder new file mode 100644 index 000000000..01db54d26 --- /dev/null +++ b/app/views/api_new/stripe_transaction_disputes/_stripe_transaction_dispute.json.jbuilder @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'stripe_transaction_dispute' + +json.net_amount do + json.partial! '/api_new/common/amount', amount: paymentable.net_amount_as_money +end + +json.gross_amount do + json.partial! '/api_new/common/amount', amount: paymentable.gross_amount_as_money +end + +json.fee_total do + json.partial! '/api_new/common/amount', amount: paymentable.fee_total_as_money +end diff --git a/app/views/api_new/stripe_transaction_disputes/object_events/_base.json.jbuilder b/app/views/api_new/stripe_transaction_disputes/object_events/_base.json.jbuilder new file mode 100644 index 000000000..2889bf3fb --- /dev/null +++ b/app/views/api_new/stripe_transaction_disputes/object_events/_base.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/subtransaction_payments/subtransaction_payment', + subtransaction_payment: event_entity.subtransaction_payment, + __expand: build_json_expansion_path_tree( + 'supporter', + 'subtransaction.transaction', + 'subtransaction.transaction.transaction_assignments' + ) \ No newline at end of file diff --git a/app/views/api_new/stripe_transaction_refunds/_stripe_transaction_refund.json.jbuilder b/app/views/api_new/stripe_transaction_refunds/_stripe_transaction_refund.json.jbuilder new file mode 100644 index 000000000..7c1478ac9 --- /dev/null +++ b/app/views/api_new/stripe_transaction_refunds/_stripe_transaction_refund.json.jbuilder @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'stripe_transaction_refund' + +json.net_amount do + json.partial! '/api_new/common/amount', amount: paymentable.net_amount_as_money +end + +json.gross_amount do + json.partial! '/api_new/common/amount', amount: paymentable.gross_amount_as_money +end + +json.fee_total do + json.partial! '/api_new/common/amount', amount: paymentable.fee_total_as_money +end diff --git a/app/views/api_new/stripe_transaction_refunds/object_events/_base.json.jbuilder b/app/views/api_new/stripe_transaction_refunds/object_events/_base.json.jbuilder new file mode 100644 index 000000000..f123bb942 --- /dev/null +++ b/app/views/api_new/stripe_transaction_refunds/object_events/_base.json.jbuilder @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/subtransaction_payments/subtransaction_payment', + subtransaction_payment: event_entity.subtransaction_payment, + __expand: build_json_expansion_path_tree( + 'supporter', + 'subtransaction', + 'subtransaction.transaction', + 'subtransaction.transaction.transaction_assignments' + ) \ No newline at end of file diff --git a/app/views/api_new/stripe_transactions/_stripe_transaction.json.jbuilder b/app/views/api_new/stripe_transactions/_stripe_transaction.json.jbuilder new file mode 100644 index 000000000..225ce91fa --- /dev/null +++ b/app/views/api_new/stripe_transactions/_stripe_transaction.json.jbuilder @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'stripe_transaction' + +json.created subtransactable.created.to_i + +json.amount do + json.partial! '/api_new/common/amount', amount: subtransactable.amount_as_money +end + +json.net_amount do + json.partial! '/api_new/common/amount', amount: subtransactable.net_amount_as_money +end diff --git a/app/views/api_new/subtransaction_payments/_subtransaction_payment.json.jbuilder b/app/views/api_new/subtransaction_payments/_subtransaction_payment.json.jbuilder new file mode 100644 index 000000000..ffe5281a5 --- /dev/null +++ b/app/views/api_new/subtransaction_payments/_subtransaction_payment.json.jbuilder @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.type 'payment' + +json.id subtransaction_payment.paymentable.houid +json.created subtransaction_payment.created.to_i + +json.legacy_id subtransaction_payment.legacy_payment_id + +json.legacy_nonprofit subtransaction_payment.nonprofit.id + +handle_expansion(:supporter, subtransaction_payment.supporter, {json: json, __expand: __expand}) +handle_expansion(:nonprofit, subtransaction_payment.nonprofit, {json: json, __expand: __expand}) +handle_expansion(:transaction, subtransaction_payment.trx, {json: json, __expand: __expand}) +handle_expansion(:subtransaction, subtransaction_payment.subtransaction, {json: json, __expand: __expand}) + +json.partial! subtransaction_payment.paymentable, as: :paymentable, __expand: __expand diff --git a/app/views/api_new/subtransactions/_subtransaction.json.jbuilder b/app/views/api_new/subtransactions/_subtransaction.json.jbuilder new file mode 100644 index 000000000..cc1a3d551 --- /dev/null +++ b/app/views/api_new/subtransactions/_subtransaction.json.jbuilder @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.id subtransaction.to_houid +json.type 'subtransaction' + +json.created subtransaction.created.to_i + +handle_expansion(:supporter, subtransaction.supporter, {json: json, __expand: __expand}) +handle_expansion(:nonprofit, subtransaction.nonprofit, {json: json, __expand: __expand}) +handle_expansion(:transaction, subtransaction.trx, {json: json, __expand: __expand}) + +handle_array_expansion(:payments, subtransaction.subtransaction_payments.ordered, {json:json, __expand: __expand, item_as: :subtransaction_payment}) do |expansion| + expansion.handle_item_expansion +end + + +json.partial! subtransaction.subtransactable, as: :subtransactable, __expand: __expand diff --git a/app/views/api_new/supporters/_supporter.json.jbuilder b/app/views/api_new/supporters/_supporter.json.jbuilder new file mode 100644 index 000000000..2ed35a7c8 --- /dev/null +++ b/app/views/api_new/supporters/_supporter.json.jbuilder @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.(supporter, :name, :email, :organization, :phone, :anonymous, :deleted) + +json.id supporter.houid + +json.object 'supporter' + +json.merged_into supporter.merged_into&.houid + +json.supporter_addresses [supporter] do |supp| + json.address supp.address + json.city supp.city + json.state_code supp.state_code + json.zip_code supp.zip_code + json.country supp.country +end + +json.legacy_id supporter.id +json.legacy_nonprofit supporter.nonprofit_id + +#json.url api_new_nonprofit_supporter_url(supporter.nonprofit.to_modern_param, supporter.to_modern_param) + +json.nonprofit supporter.nonprofit.houid diff --git a/app/views/api_new/supporters/index.json.jbuilder b/app/views/api_new/supporters/index.json.jbuilder new file mode 100644 index 000000000..f95c6c7b8 --- /dev/null +++ b/app/views/api_new/supporters/index.json.jbuilder @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.data @supporters, partial: '/api_new/supporters/supporter', as: :supporter + +json.current_page @supporters.current_page +json.first_page @supporters.first_page? +json.last_page @supporters.last_page? +json.requested_size @supporters.limit_value +json.total_count @supporters.total_count diff --git a/app/views/api_new/supporters/object_events/_base.json.jbuilder b/app/views/api_new/supporters/object_events/_base.json.jbuilder new file mode 100644 index 000000000..cbc981d08 --- /dev/null +++ b/app/views/api_new/supporters/object_events/_base.json.jbuilder @@ -0,0 +1,4 @@ +# frozen_string_literal: true + +json.partial! 'api_new/supporters/supporter', + supporter: event_entity diff --git a/app/views/api_new/supporters/show.json.jbuilder b/app/views/api_new/supporters/show.json.jbuilder new file mode 100644 index 000000000..c72fd0f33 --- /dev/null +++ b/app/views/api_new/supporters/show.json.jbuilder @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! '/api_new/supporters/supporter', supporter: @supporter diff --git a/app/views/api_new/ticket_purchases/_ticket_purchase.json.jbuilder b/app/views/api_new/ticket_purchases/_ticket_purchase.json.jbuilder new file mode 100644 index 000000000..8a8aaa422 --- /dev/null +++ b/app/views/api_new/ticket_purchases/_ticket_purchase.json.jbuilder @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.object 'ticket_purchase' + diff --git a/app/views/api_new/ticket_purchases/object_events/_base.json.jbuilder b/app/views/api_new/ticket_purchases/object_events/_base.json.jbuilder new file mode 100644 index 000000000..4c5f983f1 --- /dev/null +++ b/app/views/api_new/ticket_purchases/object_events/_base.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.partial! 'api_new/transaction_assignments/transaction_assignment', + transaction_assignment: event_entity.transaction_assignment, + __expand: build_json_expansion_path_tree( + 'transaction', + 'transaction.transaction_assignments', + 'transaction.subtransaction.payments' + ) \ No newline at end of file diff --git a/app/views/api_new/transaction_assignments/_transaction_assignment.json.jbuilder b/app/views/api_new/transaction_assignments/_transaction_assignment.json.jbuilder new file mode 100644 index 000000000..5a53fc943 --- /dev/null +++ b/app/views/api_new/transaction_assignments/_transaction_assignment.json.jbuilder @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.type 'trx_assignment' + +json.id transaction_assignment.to_houid +#json.created transaction_assignment.acreated.to_i + +handle_expansion(:supporter, transaction_assignment.supporter, {json: json, __expand: __expand}) +handle_expansion(:nonprofit, transaction_assignment.nonprofit, {json: json, __expand: __expand}) +handle_expansion(:transaction, transaction_assignment.trx, {json: json, __expand: __expand}) + + +json.partial! transaction_assignment.assignable, as: :assignable, __expand: __expand + diff --git a/app/views/api_new/transactions/_transaction.json.jbuilder b/app/views/api_new/transactions/_transaction.json.jbuilder new file mode 100644 index 000000000..aa01fb678 --- /dev/null +++ b/app/views/api_new/transactions/_transaction.json.jbuilder @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.id transaction.houid + +json.object 'transaction' + +handle_expansion(:supporter, transaction.supporter, {json: json, __expand: __expand}) + +handle_expansion(:nonprofit, transaction.nonprofit, {json: json, __expand: __expand}) + +json.created transaction.created.to_i + +json.amount do + json.partial! '/api_new/common/amount', amount: transaction.amount_as_money +end + +handle_expansion(:subtransaction, transaction.subtransaction, {json: json, __expand: __expand}) + +handle_array_expansion(:transaction_assignments, transaction.transaction_assignments, {json: json, __expand: __expand, item_as: :transaction_assignment}) do |expansion| + expansion.handle_item_expansion +end + +handle_array_expansion(:payments, transaction.payments.ordered, {json: json, __expand: __expand, item_as: :subtransaction_payment}) do |expansion| + expansion.handle_item_expansion +end + +#json.url api_nonprofit_transaction_url(transaction.nonprofit, transaction) diff --git a/app/views/api_new/transactions/index.json.jbuilder b/app/views/api_new/transactions/index.json.jbuilder new file mode 100644 index 000000000..cb94db1d8 --- /dev/null +++ b/app/views/api_new/transactions/index.json.jbuilder @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE + +json.data @transactions, partial: '/api_new/transactions/transaction', as: 'transaction', __expand: @__expand + +json.current_page @transactions.current_page +json.first_page @transactions.first_page? +json.last_page @transactions.last_page? +json.requested_size @transactions.limit_value +json.total_count @transactions.total_count diff --git a/app/views/api_new/transactions/object_events/_base.json.jbuilder b/app/views/api_new/transactions/object_events/_base.json.jbuilder new file mode 100644 index 000000000..ab96aa821 --- /dev/null +++ b/app/views/api_new/transactions/object_events/_base.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.partial! event_entity, as: :transaction, __expand: build_json_expansion_path_tree(%w(subtransaction.payments transaction_assignments) ) diff --git a/app/views/api_new/transactions/show.json.jbuilder b/app/views/api_new/transactions/show.json.jbuilder new file mode 100644 index 000000000..013a71858 --- /dev/null +++ b/app/views/api_new/transactions/show.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.partial! @transaction, as: :transaction, __expand: @__expand \ No newline at end of file diff --git a/app/views/api_new/users/_user.json.jbuilder b/app/views/api_new/users/_user.json.jbuilder new file mode 100644 index 000000000..27e4a31fd --- /dev/null +++ b/app/views/api_new/users/_user.json.jbuilder @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.object 'user' + +json.roles user.nonprofit_admin_roles do |role| + json.host role.host.to_houid +end + +json.is_super_admin user.roles.super_admins.any? diff --git a/app/views/api_new/users/current.json.jbuilder b/app/views/api_new/users/current.json.jbuilder new file mode 100644 index 000000000..ce7965ff8 --- /dev/null +++ b/app/views/api_new/users/current.json.jbuilder @@ -0,0 +1,5 @@ +# frozen_string_literal: true + +# License: AGPL-3.0-or-later WITH WTO-AP-3.0-or-later +# Full license explanation at https://github.com/houdiniproject/houdini/blob/main/LICENSE +json.partial! @user, as: :user diff --git a/app/views/billing_subscriptions/_new_modal.html.erb b/app/views/billing_subscriptions/_new_modal.html.erb deleted file mode 100644 index 0fcd7adbf..000000000 --- a/app/views/billing_subscriptions/_new_modal.html.erb +++ /dev/null @@ -1,35 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> - diff --git a/app/views/button_debug/button.html.erb b/app/views/button_debug/button.html.erb index 61a7cc2ca..b38c6f77a 100644 --- a/app/views/button_debug/button.html.erb +++ b/app/views/button_debug/button.html.erb @@ -3,7 +3,7 @@ (function() { if(document.querySelector('.commitchange-donate[data-fixed]')) return; var cc_donate = document.createElement('div'); - cc_donate.innerHTML = ""; + cc_donate.innerHTML = ""; document.body.appendChild(cc_donate); var npo = 1; var script = document.createElement('script'); diff --git a/app/views/campaigns/_schema.html.erb b/app/views/campaigns/_schema.html.erb index b83fc74f8..6db1aa924 100644 --- a/app/views/campaigns/_schema.html.erb +++ b/app/views/campaigns/_schema.html.erb @@ -1,23 +1,27 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> - diff --git a/app/views/campaigns/_settings_modal.html.erb b/app/views/campaigns/_settings_modal.html.erb index f378e66f3..c7900b310 100644 --- a/app/views/campaigns/_settings_modal.html.erb +++ b/app/views/campaigns/_settings_modal.html.erb @@ -1,4 +1,5 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> + @@ -26,37 +27,32 @@ <% else %>
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +<% if @dispute.dispute_transactions.any? %> + Withdrawal ID: + + + + + + + + + + + +<% end %> + + +<% if @dispute.dispute_transactions.count == 2 %> + Reinstatement ID: + + + + + + + + + + + +<% end %> + diff --git a/app/views/dispute_mailer/created.html.erb b/app/views/dispute_mailer/created.html.erb new file mode 100644 index 000000000..9fc37e075 --- /dev/null +++ b/app/views/dispute_mailer/created.html.erb @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +A new dispute, <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%> +has been created for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %> + +<%= render 'dispute_mailer/dispute_details'%> diff --git a/app/views/dispute_mailer/funds_reinstated.html.erb b/app/views/dispute_mailer/funds_reinstated.html.erb new file mode 100644 index 000000000..13bd89029 --- /dev/null +++ b/app/views/dispute_mailer/funds_reinstated.html.erb @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +<%= print_currency(@reinstated_transaction.payment.net_amount, "$")%> has been REINSTATED for <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%>, + for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %>. + +<%= render 'dispute_mailer/dispute_details'%> \ No newline at end of file diff --git a/app/views/dispute_mailer/funds_withdrawn.html.erb b/app/views/dispute_mailer/funds_withdrawn.html.erb new file mode 100644 index 000000000..18c11a117 --- /dev/null +++ b/app/views/dispute_mailer/funds_withdrawn.html.erb @@ -0,0 +1,4 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +<%= print_currency(@withdrawal_transaction.payment.net_amount, "$")%> has been WITHDRAWN for <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%>, + for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %>. \ No newline at end of file diff --git a/app/views/dispute_mailer/lost.html.erb b/app/views/dispute_mailer/lost.html.erb new file mode 100644 index 000000000..ec9665e10 --- /dev/null +++ b/app/views/dispute_mailer/lost.html.erb @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +The dispute, <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%>, +has been LOST for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %>. + +<%= render 'dispute_mailer/dispute_details'%> diff --git a/app/views/dispute_mailer/updated.html.erb b/app/views/dispute_mailer/updated.html.erb new file mode 100644 index 000000000..afb8920ae --- /dev/null +++ b/app/views/dispute_mailer/updated.html.erb @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +The dispute, <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%>, +has been updated for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %>. + +<%= render 'dispute_mailer/dispute_details'%> diff --git a/app/views/dispute_mailer/won.html.erb b/app/views/dispute_mailer/won.html.erb new file mode 100644 index 000000000..ca2e11249 --- /dev/null +++ b/app/views/dispute_mailer/won.html.erb @@ -0,0 +1,6 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +The dispute, <%= link_to( @stripe_dispute.stripe_dispute_id, + "https://dashboard.stripe.com/payments/"+ @dispute.charge.stripe_charge_id)%>, +has been WON for <%= link_to(@nonprofit.name + " #(#{@nonprofit.id})", @nonprofit) %>. + +<%= render 'dispute_mailer/dispute_details'%> \ No newline at end of file diff --git a/app/views/donation_mailer/_donation_changed_amount_table.html.erb b/app/views/donation_mailer/_donation_changed_amount_table.html.erb index ae1b520e9..fc937af91 100644 --- a/app/views/donation_mailer/_donation_changed_amount_table.html.erb +++ b/app/views/donation_mailer/_donation_changed_amount_table.html.erb @@ -10,7 +10,7 @@ - <% if donation.anonymous %> + <% if donation.payment&.consider_anonymous? %> <% else %> diff --git a/app/views/donation_mailer/_donation_payment_table.html.erb b/app/views/donation_mailer/_donation_payment_table.html.erb index 0b3af3616..4b76f2daf 100644 --- a/app/views/donation_mailer/_donation_payment_table.html.erb +++ b/app/views/donation_mailer/_donation_payment_table.html.erb @@ -10,7 +10,7 @@ - <% if donation.anonymous || donation.supporter.anonymous %> + <% if donation.payment&.consider_anonymous? %> <% else %> @@ -18,17 +18,17 @@ - + - + <% if donation.campaign && donation.campaign.published %> - + <% end %> @@ -84,10 +84,10 @@ <% end %> - <% if charge %> + <% if @payment&.charge %> - + <% end %> diff --git a/app/views/donation_mailer/donor_direct_debit_notification.html.erb b/app/views/donation_mailer/donor_direct_debit_notification.html.erb index 7c1ddae65..383cf1113 100644 --- a/app/views/donation_mailer/donor_direct_debit_notification.html.erb +++ b/app/views/donation_mailer/donor_direct_debit_notification.html.erb @@ -20,7 +20,7 @@ <%= t('mailer.donations.donor_direct_debit_notification.transfer_info_html', label: t('mailer.donations.donor_direct_debit_notification.transfer_label_html', nonprofit_statement: @nonprofit.statement)) %>


-<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %> +<%= render 'donation_mailer/donation_payment_table', donation: @donation, payment: @payment %> <%= render 'emails/powered_by' %> diff --git a/app/views/donation_mailer/donor_payment_notification.html.erb b/app/views/donation_mailer/donor_payment_notification.html.erb index 0a4e3d04d..37de519a8 100644 --- a/app/views/donation_mailer/donor_payment_notification.html.erb +++ b/app/views/donation_mailer/donor_payment_notification.html.erb @@ -20,9 +20,14 @@ <%= t('mailer.donations.donor_receipt.transfer_info_html', label: t('mailer.donations.donor_receipt.transfer_label_html', nonprofit_statement: @nonprofit.statement)) %>


-<%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @donation.charges.last %> +<%= render 'donation_mailer/donation_payment_table', donation: @donation, payment: @payment %> <% if @donation.recurring_donation %> +

+ +If you need to pause your donation due to financial hardship, please contact <%= @nonprofit.name %> by sending them an email at <%= link_to @reply_to, "mailto:#{@reply_to}" %> with how long you would like the pause to occur. + +

<%= t('mailer.donations.donor_receipt.recurring_donation_cancel_modify_html', management_url: edit_recurring_donation_url(@donation.recurring_donation, {t: @donation.recurring_donation.edit_token}))%>

diff --git a/app/views/donation_mailer/nonprofit_payment_notification.html.erb b/app/views/donation_mailer/nonprofit_payment_notification.html.erb index efa2673eb..35788c219 100644 --- a/app/views/donation_mailer/nonprofit_payment_notification.html.erb +++ b/app/views/donation_mailer/nonprofit_payment_notification.html.erb @@ -14,6 +14,7 @@ <%= render 'components/email/supporter_table', supporter: @donation.supporter %>
+<% @show_campaign_creator = true%> <%= render 'donation_mailer/donation_payment_table', donation: @donation, charge: @charge %> <% if @donation.recurring_donation %> diff --git a/app/views/donation_mailer/nonprofit_recurring_donation_cancellation.html.erb b/app/views/donation_mailer/nonprofit_recurring_donation_cancellation.html.erb index 3d3439e29..78d4b34ec 100644 --- a/app/views/donation_mailer/nonprofit_recurring_donation_cancellation.html.erb +++ b/app/views/donation_mailer/nonprofit_recurring_donation_cancellation.html.erb @@ -8,11 +8,6 @@ A recurring donation from one of your supporters has been cancelled. Details are This donation was cancelled by <%= @donation.recurring_donation.cancelled_by %>.

- - -

-The donor has also received a notification of the cancellation. -


<% if @charge && @charge.failure_message.present? %> diff --git a/app/views/donations/_new_modal.html.erb b/app/views/donations/_new_modal.html.erb index a03e25d23..0064a5a1a 100644 --- a/app/views/donations/_new_modal.html.erb +++ b/app/views/donations/_new_modal.html.erb @@ -28,12 +28,34 @@
- - + + +
- + + +
+ <% fee_covered_id = SecureRandom.uuid %> + + + + +
+ + + + <% if @event %><% end %> @@ -49,6 +71,15 @@ <%= render 'components/forms/submit_button', loading_text: 'Loading...', button_text: 'Next' %> + + +
+ + +
+ Transactions secured with 256-bit SSL and protected by reCAPTCHA Enterprise. The Google Privacy Policy and Terms of Service apply. +
+
@@ -63,7 +94,7 @@
+Payment ID:<%=link_to(@payment.id, nonprofits_payments_url(@nonprofit, {pid: @payment.id})) %>
+Payment Date:<%= @payment.date %>
+Payment Amount:<%= print_currency(@payment.gross_amount, "$") %>
+Dispute Id:<%= @dispute.id %>
+Dispute Amount:<%= print_currency(@dispute.gross_amount, "$") %>
Dispute Reason:<%= @dispute.reason %>
+Dispute Status:<%= @dispute.status %>
+EVIDENCE DUE DATE:<%= @stripe_dispute.evidence_due_date %>
<%=link_to(@dispute.dispute_transactions.first.payment.id, nonprofits_payments_url(@nonprofit, {pid: @dispute.dispute_transactions.first.payment.id})) %>
+Withdrawal Date:<%= @dispute.dispute_transactions.first.payment.date %>
+Withdrawal Amount:<%= print_currency(@dispute.dispute_transactions.first.payment.net_amount, "$") %>
<%=link_to(@dispute.dispute_transactions.second.payment.id, nonprofits_payments_url(@nonprofit, {pid: @dispute.dispute_transactions.second.payment.id})) %>
+Reinstatement Date:<%= @dispute.dispute_transactions.second.payment.date %>
+Reinstatement Amount:<%= print_currency(@dispute.dispute_transactions.second.payment.net_amount, "$") %>
Donor NameAnonymous<%= donation.supporter.name %>
<%= t('mailer.donations.donor_name') %>Anonymous<%= donation.supporter.name %>
<%= t('donation.amount') %> <%= print_currency(charge.try(:amount) || donation.amount, donation.nonprofit.currency_symbol) %> <%= print_currency(@payment&.charge&.amount || donation.amount, donation.nonprofit.currency_symbol) %>
<%= t('donation.date') %><%= date_and_time(charge.try(:created_at) || donation.created_at, donation.nonprofit.timezone) %><%= date_and_time(@payment&.charge.try(:created_at) || donation.created_at, donation.nonprofit.timezone) %>
<%= t('donation.campaign') %><%= donation.campaign.name %>(Campaign Id: <%= donation.campaign.id%>, Creator: <%= donation.campaign.profile.user.email %>)<%= donation.campaign.name %>(Campaign Id: <%= donation.campaign.id%><% if @show_campaign_creator %>, Creator: <%= donation.campaign.profile.user.email %><% end %>)
<%= t('donation.payment_id') %> <%= GetData.chain(donation.payment, :id) %> <%= GetData.chain(@payment.id) %>
- + diff --git a/app/views/donations/_new_offline_modal.html.erb b/app/views/donations/_new_offline_modal.html.erb index f25cd9bc2..6385f2c70 100644 --- a/app/views/donations/_new_offline_modal.html.erb +++ b/app/views/donations/_new_offline_modal.html.erb @@ -13,7 +13,7 @@ <%= render 'organizer' %> @@ -135,6 +186,5 @@ <%= render 'nonprofits/donate/modal' %> -<%= render 'tickets/new_modal', profile: current_user ? current_user.profile : nil %> -<%= render 'contact_organizer_modal' %> -<%= render 'common/email_share_modal', fundraiser: @event.name, fundraiser_url: @url %> +<%= render 'tickets/new_modal', profile: current_user ? current_user.profile : nil, hide_cover_fees_option: !!@event.hide_cover_fees? %> +<%= render 'contact_organizer_modal' %> \ No newline at end of file diff --git a/app/views/events/stats.html.erb b/app/views/events/stats.html.erb index 2a49851fb..363ce1440 100644 --- a/app/views/events/stats.html.erb +++ b/app/views/events/stats.html.erb @@ -1,19 +1,19 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> <%= content_for(:title) {"#{@event.name} - #{@event.nonprofit.name}".html_safe} %> -<%= content_for(:meta_description) {raw @event.summary} %> +<%= content_for(:meta_description) {@event.summary} %> <%= content_for(:stylesheets) {stylesheet_link_tag 'events/stats/page'} %> <% content_for(:footer_hidden) {'hidden'} %> <% content_for(:fixed_position_cta_hidden) {'hidden'} %> <%= content_for :facebook_tags do %> - - + + <% end %> <%= content_for :twitter_tags do %> - - + + <% end %> diff --git a/app/views/export_mailer/export_cancelled_recurring_donations_monthly_completed_notification.html.erb b/app/views/export_mailer/export_cancelled_recurring_donations_monthly_completed_notification.html.erb new file mode 100644 index 000000000..d3528726c --- /dev/null +++ b/app/views/export_mailer/export_cancelled_recurring_donations_monthly_completed_notification.html.erb @@ -0,0 +1,10 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

Your export from <%= Format::Date.simple @export.created_at %> was completed successfully.

+ +

To view your CSV report, visit: your report.

+ +

Note: your generated CSV file will be automatically deleted from our servers after seven days. Don't worry; you can always visit your recurring donations panel and export a new CSV file.

+ +

If you have any questions about this export, please contact <%= Settings.devise.mailer_sender %>.

+ +<%= render 'emails/sig' %> diff --git a/app/views/export_mailer/export_cancelled_recurring_donations_monthly_failed_notification.html.erb b/app/views/export_mailer/export_cancelled_recurring_donations_monthly_failed_notification.html.erb new file mode 100644 index 000000000..9ef1ece8e --- /dev/null +++ b/app/views/export_mailer/export_cancelled_recurring_donations_monthly_failed_notification.html.erb @@ -0,0 +1,13 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

Your export from <%= Format::Date.simple @export.created_at %> did not succeed due to a technical error.

+

While CommitChange engineers have been notified, don't hesitate to contact + <%= Settings.devise.mailer_sender %> with the following information:

+ +
    +
  • Export ID: <%= @export.id %>
  • +
  • User ID: <%= @export.user_id %>
  • +
  • Nonprofit ID: <%= @export.nonprofit_id %>
  • +
  • Exception: <%= @export.exception %>
  • +
+ +<%= render 'emails/sig' %> diff --git a/app/views/export_mailer/export_failed_recurring_donations_monthly_completed_notification.html.erb b/app/views/export_mailer/export_failed_recurring_donations_monthly_completed_notification.html.erb new file mode 100644 index 000000000..d3528726c --- /dev/null +++ b/app/views/export_mailer/export_failed_recurring_donations_monthly_completed_notification.html.erb @@ -0,0 +1,10 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

Your export from <%= Format::Date.simple @export.created_at %> was completed successfully.

+ +

To view your CSV report, visit: your report.

+ +

Note: your generated CSV file will be automatically deleted from our servers after seven days. Don't worry; you can always visit your recurring donations panel and export a new CSV file.

+ +

If you have any questions about this export, please contact <%= Settings.devise.mailer_sender %>.

+ +<%= render 'emails/sig' %> diff --git a/app/views/export_mailer/export_failed_recurring_donations_monthly_failed_notification.html.erb b/app/views/export_mailer/export_failed_recurring_donations_monthly_failed_notification.html.erb new file mode 100644 index 000000000..9ef1ece8e --- /dev/null +++ b/app/views/export_mailer/export_failed_recurring_donations_monthly_failed_notification.html.erb @@ -0,0 +1,13 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

Your export from <%= Format::Date.simple @export.created_at %> did not succeed due to a technical error.

+

While CommitChange engineers have been notified, don't hesitate to contact + <%= Settings.devise.mailer_sender %> with the following information:

+ +
    +
  • Export ID: <%= @export.id %>
  • +
  • User ID: <%= @export.user_id %>
  • +
  • Nonprofit ID: <%= @export.nonprofit_id %>
  • +
  • Exception: <%= @export.exception %>
  • +
+ +<%= render 'emails/sig' %> diff --git a/app/views/layouts/_app_data.html.erb b/app/views/layouts/_app_data.html.erb index c6d273d15..790e3a8f3 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' %> @@ -16,21 +15,20 @@ var app = { , host_with_port: "//<%= request.host_with_port %>" , google_api: "<%= ENV['GOOGLE_API_KEY'] %>" , google_auth_client_id: "<%= ENV['GOOGLE_AUTH_CLIENT_ID'] %>" -, autocomplete: <%= @nonprofit ? @nonprofit.autocomplete_supporter_address : 'undefined'%> +, autocomplete: <%= @nonprofit ? (@nonprofit.autocomplete_supporter_address?) : false %> , facebook_app_id: "<%= ENV['FACEBOOK_APP_ID'] %>" , map_provider: "<%= Settings.maps&.provider %>" , map_provider_options: <%= Settings.maps&.options ? raw(Settings.maps.options.to_json) : {} %> , editor: "<%= Settings.page_editor.editor%>" <% if Settings&.page_editor&.editor == 'froala' and Settings&.page_editor&.editor_options&.froala_key %>, froala_key: "<%= Settings.page_editor.editor_options.froala_key %>"<% end %> <%if @nonprofit&.currency_symbol %>, currency_symbol: "<%= @nonprofit.currency_symbol%>"<% end %> + , recaptcha_site_key: "<%= ENV['RECAPTCHA_SITE_KEY'] %>" }; var ENV = { nonprofitID: <%= @nonprofit ? @nonprofit.id : 'undefined' %> , nonprofitTimezone: "<%= @nonprofit ? @nonprofit.timezone : '' %>" , support_user_id: 540 -, feeRate: <%= print_percent(CalculateFees::BaseFeeRate) %> -, perTransaction: <%= CalculateFees::PerTransaction %> }; diff --git a/app/views/layouts/_footer.html.erb b/app/views/layouts/_footer.html.erb index 2f633391e..cc7f00563 100755 --- a/app/views/layouts/_footer.html.erb +++ b/app/views/layouts/_footer.html.erb @@ -3,7 +3,6 @@ <%= render 'common/confirm_email_modal' %> <% end %> <% else %> - <%= render 'common/choose_role_modal' %> <%= render 'users/signup_modal' %> <%= render 'users/login_modal' %> <% end %> @@ -12,7 +11,6 @@ -<%= render 'common/onboarding_modals' %> <%= render 'common/confirmation' %>
@@ -34,25 +32,7 @@ <% if yield(:footer_hidden) != 'hidden' %>
+ <%= render 'components/guest_footer' unless current_user %> <%= render 'components/footer_sub' %>
<% end %> - - diff --git a/app/views/layouts/_javascripts.html.erb b/app/views/layouts/_javascripts.html.erb index 6ca0c96ce..da8b441e1 100644 --- a/app/views/layouts/_javascripts.html.erb +++ b/app/views/layouts/_javascripts.html.erb @@ -3,36 +3,23 @@ <%= render 'layouts/app_data' %> -<% if Settings.payment_provider.stripe_proprietary_v2_js %> - -<% else %> - <%= IncludeAsset.js "/client/js/stripe_wrapper/page.js" %> -<% end %> + <%= IncludeAsset.js "/client/js/page.js" %> - <%= IncludeAsset.js '/client/js/i18n.js' %> - - <%= yield :javascripts %> - - - - - -<% if current_role?([:nonprofit_associate, :nonprofit_admin, :super_admin]) && yield(:hide_nav_beacon) != 'true' %> - +<% if current_role?([:nonprofit_associate, :nonprofit_admin, :super_admin]) && yield(:hide_nav_beacon) != 'true' && yield(:left_align_nav_beacon) != 'true' %> <%= render 'layouts/nav_beacon' %> - - +<% elsif yield(:left_align_nav_beacon) == 'true' %> + <%= render 'layouts/left_aligned_nav_beacon' %> <% end %> diff --git a/app/views/layouts/_left_aligned_nav_beacon.html.erb b/app/views/layouts/_left_aligned_nav_beacon.html.erb new file mode 100644 index 000000000..22ba6c924 --- /dev/null +++ b/app/views/layouts/_left_aligned_nav_beacon.html.erb @@ -0,0 +1,16 @@ + + diff --git a/app/views/layouts/_meta_tags.html.erb b/app/views/layouts/_meta_tags.html.erb index a3b98d78e..2b12de2f9 100644 --- a/app/views/layouts/_meta_tags.html.erb +++ b/app/views/layouts/_meta_tags.html.erb @@ -38,6 +38,17 @@ <% end %> + +<% if Rails.env == 'development' %> + +<% else %> + +<% end%> + + +<%= yield :recaptcha_js %> + + <%= csrf_meta_tags %> <%= favicon_link_tag %> diff --git a/app/views/layouts/_nav_beacon.html.erb b/app/views/layouts/_nav_beacon.html.erb index d9e2fa064..19b42fb27 100644 --- a/app/views/layouts/_nav_beacon.html.erb +++ b/app/views/layouts/_nav_beacon.html.erb @@ -1,2 +1,15 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -<%# put a nav beacon script here if you want it %> \ No newline at end of file + + diff --git a/app/views/layouts/_top_nav.html.erb b/app/views/layouts/_top_nav.html.erb deleted file mode 100644 index f13f41b68..000000000 --- a/app/views/layouts/_top_nav.html.erb +++ /dev/null @@ -1,21 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -<%= content_for(:side_nav_hidden) {'hidden'} %> - - \ No newline at end of file diff --git a/app/views/layouts/_user_menu.html.erb b/app/views/layouts/_user_menu.html.erb index 1f8bb3b3a..7bfda6516 100644 --- a/app/views/layouts/_user_menu.html.erb +++ b/app/views/layouts/_user_menu.html.erb @@ -39,10 +39,6 @@ Login - - - Sign up - <% end %> diff --git a/app/views/layouts/apified.html.erb b/app/views/layouts/apified.html.erb index f5ffbf514..b5c4a7872 100644 --- a/app/views/layouts/apified.html.erb +++ b/app/views/layouts/apified.html.erb @@ -3,6 +3,7 @@ + <%= "#{yield(:title)} - #{Settings.general.name}" %> diff --git a/app/views/layouts/btn.html.erb b/app/views/layouts/btn.html.erb new file mode 100644 index 000000000..8fe7824bc --- /dev/null +++ b/app/views/layouts/btn.html.erb @@ -0,0 +1,25 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> + + + + + + + + <%= yield :stylesheets %> + + <%= favicon_link_tag %> + + + + + +<%= yield %> +<%= yield :javascripts %> + + + diff --git a/app/views/layouts/email.html.erb b/app/views/layouts/email.html.erb index 911af7cec..45bd64708 100644 --- a/app/views/layouts/email.html.erb +++ b/app/views/layouts/email.html.erb @@ -1,13 +1,17 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> - - - - - -
- <%= yield %> -
- - + + + + <%= stylesheet_link_tag("emails/page") %> + + + + +
+ <%= yield %> +
+ + + \ No newline at end of file diff --git a/app/views/layouts/embed.html.erb b/app/views/layouts/embed.html.erb index e24a24799..a1e35924e 100644 --- a/app/views/layouts/embed.html.erb +++ b/app/views/layouts/embed.html.erb @@ -6,13 +6,11 @@ <%= yield(:title) || "Donate with #{Settings.general.name}" %> <%= render 'layouts/stylesheets' %> - <%= yield :stylesheets %> - + <%= yield :recaptcha_js %> <%= favicon_link_tag %> - <%= csrf_meta_tags %> - + @@ -20,9 +18,6 @@ <%= yield %> <%= render 'layouts/javascripts' %> -<%= yield :javascripts %> - - diff --git a/app/views/layouts/mailer.html.erb b/app/views/layouts/mailer.html.erb new file mode 100644 index 000000000..0228f2be8 --- /dev/null +++ b/app/views/layouts/mailer.html.erb @@ -0,0 +1,18 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> + + + + + + <%= stylesheet_link_tag("emails/page") %> + + + + +
+ <%= yield %> + <%= render "emails/powered_by" %> +
+ + + \ No newline at end of file diff --git a/app/views/mailchimp/list.json.jbuilder b/app/views/mailchimp/list.json.jbuilder new file mode 100644 index 000000000..fad14c135 --- /dev/null +++ b/app/views/mailchimp/list.json.jbuilder @@ -0,0 +1,13 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later +json.email_address @supporter.email +json.status 'subscribed' + +json.merge_fields do + json.F_NAME @supporter.calculated_first_name + json.L_NAME @supporter.calculated_last_name + + @supporter.recurring_donations.active.order('start_date DESC').each_with_index do |item, i| + json.set! "RD_URL_#{i+1}", # we use i+1 because we want this to start at RD_URL_1 + edit_recurring_donation_url(id: item.id, t: item.edit_token) + end +end \ No newline at end of file diff --git a/app/views/mailchimp/nonprofit_user_subscribe.json.jbuilder b/app/views/mailchimp/nonprofit_user_subscribe.json.jbuilder new file mode 100644 index 000000000..a6bf3853e --- /dev/null +++ b/app/views/mailchimp/nonprofit_user_subscribe.json.jbuilder @@ -0,0 +1,10 @@ +# License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later + +json.email_address @user.email +json.status 'subscribed' + +json.merge_fields do + json.NP_ID @nonprofit.id + json.NP_SUPP @nonprofit.supporters.not_deleted.count + json.FNAME @user.calculated_first_name || "" +end diff --git a/app/views/nonprofit_mailer/failed_verification_notice.html.erb b/app/views/nonprofit_mailer/failed_verification_notice.html.erb deleted file mode 100644 index ebeca0d6e..000000000 --- a/app/views/nonprofit_mailer/failed_verification_notice.html.erb +++ /dev/null @@ -1,16 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -

Dear <%= @nonprofit.name %>,

- -

We submitted your verification data, but our payment processor came back to us saying that they weren't able to verify your info. This usually happens because the name or birthdate provided in the form doesn't match your Social Security entry exactly. There is still hope, however! If you return to the Payouts Page, you can try one of two solutions:

- -
    -
  1. Securely enter your full Social Security number -- this almost always resolves any verification issues.
  2. -
  3. Re-enter the information on the basic form -- you can re-enter your own information or use info from another account holder.
  4. -
- -

Visit your payouts page to get everything sorted out.

- -

As always, if you have any questions, please send us a note at <%= Settings.devise.mailer_sender %>

- -<%= render 'emails/sig' %> - diff --git a/app/views/nonprofit_mailer/first_charge_email.html.erb b/app/views/nonprofit_mailer/first_charge_email.html.erb new file mode 100644 index 000000000..a7383954f --- /dev/null +++ b/app/views/nonprofit_mailer/first_charge_email.html.erb @@ -0,0 +1,31 @@ +<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> +

+Congratulations on receiving your first transaction on <%= Settings.general.name %>! +

+
+

+Whenever you receive money on <%= Settings.general.name %>, it will go into your account’s balance. Payouts are made to your organization’s bank account automatically at the beginning of each month, or at any time with the click of a button. +

+<% unless @nonprofit.bank_account&.allows_payout? -%> +
+

+Before you can make a payout, you need to connect your organization's bank account for withdrawals, allowing money to transfer from your account balance on <%= Settings.general.name %> directly to your organization. +

+ +
+

You can connect your bank account on this page:

+ +
+

+ <% url = nonprofits_payouts_url(@nonprofit) -%> + <%= link_to(url, url) -%> +

+
+<% end -%> +<% unless @nonprofit.stripe_account&.payouts_enabled -%> +<% url = nonprofits_payouts_url(@nonprofit) -%> +

Stripe is requested additional verification before funds can be paid out. You can access your secure verification form here: <%= link_to(url, url) %>.

+<% end -%> + +

If you’d like to schedule a free strategy session or web audit for fundraising tips and best practices, please send us an email at support@commitchange.com. In addition to our online help center, we also offer free online training to all of our nonprofits.

+<%= render 'emails/sig' %> diff --git a/app/views/nonprofit_mailer/setup_verification.html.erb b/app/views/nonprofit_mailer/setup_verification.html.erb deleted file mode 100644 index 796602150..000000000 --- a/app/views/nonprofit_mailer/setup_verification.html.erb +++ /dev/null @@ -1,30 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -

-Congratulations on receiving your first transaction on <%= Settings.general.name %>! -

-
-

-Whenever you receive money on <%= Settings.general.name %>, it will go into your account’s balance. Payouts can be made to your organization’s bank account automatically at the beginning of each month, or at any time with the click of a button. -

- -
-

-Before you can make a payout, you need to connect your organization's bank account for withdrawals, allowing money to transfer from your account balance on <%= Settings.general.name %> directly to your organization. -

- -
-

You can connect your bank account on this page:

- -
-

- <% url = nonprofits_payouts_url(@nonprofit) %> - <%= url %> -

- -
-

-You have a 7 day window to continue processing transactions without a connected bank account. After that, transactions will become disabled. So, try to get that bank account connected as soon as you can. -

- -
-<%= render 'emails/sig' %> diff --git a/app/views/nonprofit_mailer/successful_verification_notice.html.erb b/app/views/nonprofit_mailer/successful_verification_notice.html.erb deleted file mode 100644 index 1802b1d6c..000000000 --- a/app/views/nonprofit_mailer/successful_verification_notice.html.erb +++ /dev/null @@ -1,12 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -

Dear <%= @nonprofit.name %>,

- -

Congrats, our payment processor successfully marked you as verified!

- -<% if @nonprofit.bank_account && !@nonprofit.bank_account.pending_verification %> -

Now that you have both your bank account and identity verification taken care of, you can withdraw your available balance of payments at any time. Visit your payout page to learn more.

-<% else %> -

Be sure to connect and verify your bank account so that you can withdraw your available balance of donations in the future. Visit your payout page to learn more.

-<% end %> - -<%= render 'emails/sig' %> diff --git a/app/views/nonprofit_mailer/supporter_message.html.erb b/app/views/nonprofit_mailer/supporter_message.html.erb deleted file mode 100644 index 766743e03..000000000 --- a/app/views/nonprofit_mailer/supporter_message.html.erb +++ /dev/null @@ -1,5 +0,0 @@ -<%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -
Amount$$
Supporter
- <%= raw @message %> - <%= render 'nonprofits/email/footer' %> -
diff --git a/app/views/nonprofit_mailer/welcome.html.erb b/app/views/nonprofit_mailer/welcome.html.erb index f36108a4c..d44f875f3 100644 --- a/app/views/nonprofit_mailer/welcome.html.erb +++ b/app/views/nonprofit_mailer/welcome.html.erb @@ -1,6 +1,6 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -

Welcome to <%= Settings.general.name %>! We’re excited to have you on board.

+

Welcome to <%= Settings.general.name %>! We're excited to have you on board.


@@ -17,9 +17,7 @@ Before we get into the good stuff, we'd like you to click this link to verify yo

-
-

-Here are a few things you can do right away to get up and running: +

Next, you'll want to <%= link_to 'verify your account', verification_nonprofits_stripe_account_url(@nonprofit)%>, through our payment processor, Stripe. (direct URL: <%=verification_nonprofits_stripe_account_url(@nonprofit)%>) You'll need to complete verification to be able to accept online donations; once you're verified, here are a few things you can do right away to get up and running:

    @@ -35,3 +33,4 @@ If you need help or have any questions, our support team is just a phone call or

    <%= render 'emails/sig' %> + diff --git a/app/views/nonprofits/_achievements.html.erb b/app/views/nonprofits/_achievements.html.erb index 47e90511f..fbd8681cd 100644 --- a/app/views/nonprofits/_achievements.html.erb +++ b/app/views/nonprofits/_achievements.html.erb @@ -1,11 +1,11 @@ <%- # License: AGPL-3.0-or-later WITH Web-Template-Output-Additional-Permission-3.0-or-later -%> -<% unless @nonprofit.achievements.join.blank? %> +<% unless @nonprofit.achievements&.join.blank? %>