diff --git a/.erb-lint.yml b/.erb_lint.yml
similarity index 100%
rename from .erb-lint.yml
rename to .erb_lint.yml
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 92a9a83624..0fd34f2603 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -53,20 +53,49 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa
Bank Users 🏦
+ Pawnee Diaper Bank
+ A fully set up bank with items, storage locations, donations, distributions, requests, etc.
+ The bank has multiple partners associated with it.
```
Organization Admin
- Email: org_admin1@example.com
+ Email: org_admin1@example.com
Password: password!
User
Email: user_1@example.com
Password: password!
```
+
+ Second City Essentials Bank
+ A fully set up bank with items, storage locations, donations, distributions, requests, etc.
+ The bank has four items unique to it (named Second City Item #).
+ ```
+ Organization Admin
+ Email: second_city_admin@example.com
+ Password: password!
+
+ User
+ Email: second_city_user@example.com
+ Password: password!
+ ```
+
+ SF Diaper Bank
+ A bank which has just been accepted and so is not fully set up. It lacks many of the records the other banks have.
+ ```
+ Organization Admin
+ Email: org_admin2@example.com
+ Password: password!
+
+ User
+ Email: user_2@example.com
+ Password: password!
+ ```
Partner Users 👥
+ Partners in Pawnee Diaper Bank partner groups
```
Verified Partner
Email: verified@example.com
@@ -87,10 +116,17 @@ Make sure to install **Ubuntu** as your Linux distribution. (This should be defa
Waiting Approval Partner
Email: waiting@example.com
Password: password!
-
- Another approved partner (with all groups):
+
+ Another verified partner (in second partner group):
Email: approved_2@example.com
- Pasword: password!
+ Password: password!
+ ```
+
+ Partners in Second City Essentials Bank partner group
+ ```
+ Verified partner
+ Email: second_city_senior_center@example.com
+ Password: password!
```
@@ -221,7 +257,7 @@ Before submitting a pull request, run all tests and lints. Fix any broken tests
- Once your first PR has been merged, all commits pushed to an open PR will also run these workflows.
#### Local testing
-- Run all lints with `bin/lint`.
+- Run all lints with `bin/lint`. (You can lint a single file/folder with `bin/lint {path_to_folder_or_file}`.)
- Run all tests with `bundle exec rspec`
- You can run a single test with `bundle exec rspec {path_to_test_name}_spec.rb` or on a specific line by appending `:LineNumber`
- If you need to skip a failing test, place `pending("Reason you are skipping the test")` into the `it` block rather than skipping with `xit`. This will allow rspec to deliver the error message without causing the test suite to fail.
diff --git a/Gemfile b/Gemfile
index 1744d1218e..1b9716089a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -14,7 +14,7 @@ gem "pg", "~> 1.5.7"
# Web server.
gem "puma"
# Rails web framework.
-gem "rails", "7.1.3.4"
+gem "rails", "7.2.2"
###### MODELS / DATABASE #######
@@ -100,6 +100,8 @@ gem "jwt"
gem "newrelic_rpm"
# Used to manage periodic cron-like jobs
gem "clockwork"
+# Speed up app boot time by caching expensive operations
+gem 'bootsnap', require: false
##### DEPENDENCY PINS ######
# These are gems that aren't used directly, only as dependencies for other gems.
diff --git a/Gemfile.lock b/Gemfile.lock
index b84187030e..db952483cb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -2,80 +2,77 @@ GEM
remote: https://rubygems.org/
specs:
Ascii85 (1.1.1)
- actioncable (7.1.3.4)
- actionpack (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ actioncable (7.2.2)
+ actionpack (= 7.2.2)
+ activesupport (= 7.2.2)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
- actionmailbox (7.1.3.4)
- actionpack (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
- mail (>= 2.7.1)
- net-imap
- net-pop
- net-smtp
- actionmailer (7.1.3.4)
- actionpack (= 7.1.3.4)
- actionview (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activesupport (= 7.1.3.4)
- mail (~> 2.5, >= 2.5.4)
- net-imap
- net-pop
- net-smtp
+ actionmailbox (7.2.2)
+ actionpack (= 7.2.2)
+ activejob (= 7.2.2)
+ activerecord (= 7.2.2)
+ activestorage (= 7.2.2)
+ activesupport (= 7.2.2)
+ mail (>= 2.8.0)
+ actionmailer (7.2.2)
+ actionpack (= 7.2.2)
+ actionview (= 7.2.2)
+ activejob (= 7.2.2)
+ activesupport (= 7.2.2)
+ mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
- actionpack (7.1.3.4)
- actionview (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ actionpack (7.2.2)
+ actionview (= 7.2.2)
+ activesupport (= 7.2.2)
nokogiri (>= 1.8.5)
racc
- rack (>= 2.2.4)
+ rack (>= 2.2.4, < 3.2)
rack-session (>= 1.0.1)
rack-test (>= 0.6.3)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- actiontext (7.1.3.4)
- actionpack (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ useragent (~> 0.16)
+ actiontext (7.2.2)
+ actionpack (= 7.2.2)
+ activerecord (= 7.2.2)
+ activestorage (= 7.2.2)
+ activesupport (= 7.2.2)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
- actionview (7.1.3.4)
- activesupport (= 7.1.3.4)
+ actionview (7.2.2)
+ activesupport (= 7.2.2)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
- activejob (7.1.3.4)
- activesupport (= 7.1.3.4)
+ activejob (7.2.2)
+ activesupport (= 7.2.2)
globalid (>= 0.3.6)
- activemodel (7.1.3.4)
- activesupport (= 7.1.3.4)
- activerecord (7.1.3.4)
- activemodel (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ activemodel (7.2.2)
+ activesupport (= 7.2.2)
+ activerecord (7.2.2)
+ activemodel (= 7.2.2)
+ activesupport (= 7.2.2)
timeout (>= 0.4.0)
- activestorage (7.1.3.4)
- actionpack (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ activestorage (7.2.2)
+ actionpack (= 7.2.2)
+ activejob (= 7.2.2)
+ activerecord (= 7.2.2)
+ activesupport (= 7.2.2)
marcel (~> 1.0)
- activesupport (7.1.3.4)
+ activesupport (7.2.2)
base64
+ benchmark (>= 0.3)
bigdecimal
- concurrent-ruby (~> 1.0, >= 1.0.2)
+ concurrent-ruby (~> 1.0, >= 1.3.1)
connection_pool (>= 2.2.5)
drb
i18n (>= 1.6, < 2)
+ logger (>= 1.4.2)
minitest (>= 5.1)
- mutex_m
- tzinfo (~> 2.0)
+ securerandom (>= 0.3)
+ tzinfo (~> 2.0, >= 2.0.5)
addressable (2.8.7)
public_suffix (>= 2.0.2, < 7.0)
afm (0.2.2)
@@ -96,6 +93,7 @@ GEM
nokogiri (~> 1, >= 1.10.8)
base64 (0.2.0)
bcrypt (3.1.20)
+ benchmark (0.4.0)
better_errors (2.10.1)
erubi (>= 1.0.0)
rack (>= 0.9.0)
@@ -111,6 +109,8 @@ GEM
bindex (0.8.1)
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
+ bootsnap (1.18.4)
+ msgpack (~> 1.2)
bootstrap (5.2.3)
autoprefixer-rails (>= 9.1.0)
popper_js (>= 2.11.6, < 3)
@@ -120,7 +120,7 @@ GEM
bugsnag (6.27.1)
concurrent-ruby (~> 1.0)
builder (3.3.0)
- bullet (8.0.0)
+ bullet (7.2.0)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
capybara (3.40.0)
@@ -143,7 +143,7 @@ GEM
coderay (1.1.3)
concurrent-ruby (1.3.4)
connection_pool (2.4.1)
- coverband (6.1.4)
+ coverband (6.1.2)
redis (>= 3.0)
crack (1.0.0)
bigdecimal
@@ -157,14 +157,14 @@ GEM
activerecord (>= 5.a)
database_cleaner-core (~> 2.0.0)
database_cleaner-core (2.0.1)
- date (3.3.4)
+ date (3.4.0)
debug (1.9.2)
irb (~> 1.10)
reline (>= 0.3.8)
debug_inspector (1.2.0)
- delayed_job (4.1.12)
- activesupport (>= 3.0, < 8.0)
- delayed_job_active_record (4.1.10)
+ delayed_job (4.1.13)
+ activesupport (>= 3.0, < 9.0)
+ delayed_job_active_record (4.1.8)
activerecord (>= 3.0, < 8.0)
delayed_job (>= 3.0, < 5)
delayed_job_web (1.4.4)
@@ -218,10 +218,10 @@ GEM
smart_properties
erubi (1.13.0)
execjs (2.10.0)
- factory_bot (6.4.5)
+ factory_bot (6.5.0)
activesupport (>= 5.0.0)
- factory_bot_rails (6.4.3)
- factory_bot (~> 6.4)
+ factory_bot_rails (6.4.4)
+ factory_bot (~> 6.5)
railties (>= 5.0.0)
faker (3.4.2)
i18n (>= 1.8.11, < 2)
@@ -259,11 +259,11 @@ GEM
ffi (1.17.0-x86_64-darwin)
ffi (1.17.0-x86_64-linux-gnu)
filterrific (5.2.5)
- flipper (1.3.2)
+ flipper (1.3.1)
concurrent-ruby (< 2)
- flipper-active_record (1.3.2)
- activerecord (>= 4.2, < 9)
- flipper (~> 1.3.2)
+ flipper-active_record (1.3.0)
+ activerecord (>= 4.2, < 8)
+ flipper (~> 1.3.0)
flipper-ui (1.3.1)
erubi (>= 1.0.0, < 2.0.0)
flipper (~> 1.3.1)
@@ -310,18 +310,18 @@ GEM
image_processing (1.13.0)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
- importmap-rails (2.0.1)
+ importmap-rails (2.0.3)
actionpack (>= 6.0.0)
activesupport (>= 6.0.0)
railties (>= 6.0.0)
- io-console (0.7.2)
- irb (1.14.1)
+ io-console (0.8.0)
+ irb (1.14.3)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jbuilder (2.13.0)
actionview (>= 5.0.0)
activesupport (>= 5.0.0)
- json (2.7.5)
+ json (2.9.1)
jwt (2.9.1)
base64
kaminari (1.2.2)
@@ -348,7 +348,7 @@ GEM
listen (3.9.0)
rb-fsevent (~> 0.10, >= 0.10.3)
rb-inotify (~> 0.9, >= 0.9.10)
- logger (1.6.0)
+ logger (1.6.3)
lograge (0.14.0)
actionpack (>= 4)
activesupport (>= 4)
@@ -373,7 +373,7 @@ GEM
method_source (1.1.0)
mini_magick (4.13.2)
mini_mime (1.1.5)
- minitest (5.25.2)
+ minitest (5.25.4)
monetize (1.12.0)
money (~> 6.12)
money (6.16.0)
@@ -383,16 +383,16 @@ GEM
monetize (~> 1.9)
money (~> 6.13)
railties (>= 3.0)
+ msgpack (1.7.5)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
multipart-post (2.4.1)
mustermann (3.0.0)
ruby2_keywords (~> 0.0.1)
- mutex_m (0.3.0)
nenv (0.3.0)
net-http-persistent (4.0.2)
connection_pool (~> 2.2)
- net-imap (0.4.12)
+ net-imap (0.5.0)
date
net-protocol
net-pop (0.1.2)
@@ -401,13 +401,13 @@ GEM
timeout
net-smtp (0.5.0)
net-protocol
- newrelic_rpm (9.13.0)
- nio4r (2.7.3)
- nokogiri (1.16.7-arm64-darwin)
+ newrelic_rpm (9.16.0)
+ nio4r (2.7.4)
+ nokogiri (1.17.2-arm64-darwin)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-darwin)
+ nokogiri (1.17.2-x86_64-darwin)
racc (~> 1.4)
- nokogiri (1.16.7-x86_64-linux)
+ nokogiri (1.17.2-x86_64-linux)
racc (~> 1.4)
notiffany (0.1.3)
nenv (~> 0.1)
@@ -438,11 +438,11 @@ GEM
capybara (>= 1.1)
rspec (>= 2.14)
orm_adapter (0.5.0)
- paper_trail (15.1.0)
+ paper_trail (15.2.0)
activerecord (>= 6.1)
request_store (~> 1.4)
parallel (1.26.3)
- parser (3.3.5.1)
+ parser (3.3.6.0)
ast (~> 2.4.1)
racc
pdf-core (0.9.0)
@@ -479,36 +479,37 @@ GEM
pry-stack_explorer (0.6.1)
binding_of_caller (~> 1.0)
pry (~> 0.13)
- psych (5.1.2)
+ psych (5.2.2)
+ date
stringio
public_suffix (6.0.1)
- puma (6.4.3)
+ puma (6.5.0)
nio4r (~> 2.0)
racc (1.8.1)
- rack (2.2.10)
- rack-protection (3.1.0)
- rack (~> 2.2, >= 2.2.4)
- rack-session (1.0.2)
- rack (< 3)
+ rack (3.1.8)
+ rack-protection (4.0.0)
+ base64 (>= 0.1.0)
+ rack (>= 3.0.0, < 4)
+ rack-session (2.0.0)
+ rack (>= 3.0.0)
rack-test (2.1.0)
rack (>= 1.3)
- rackup (1.0.0)
- rack (< 3)
- webrick
- rails (7.1.3.4)
- actioncable (= 7.1.3.4)
- actionmailbox (= 7.1.3.4)
- actionmailer (= 7.1.3.4)
- actionpack (= 7.1.3.4)
- actiontext (= 7.1.3.4)
- actionview (= 7.1.3.4)
- activejob (= 7.1.3.4)
- activemodel (= 7.1.3.4)
- activerecord (= 7.1.3.4)
- activestorage (= 7.1.3.4)
- activesupport (= 7.1.3.4)
+ rackup (2.2.0)
+ rack (>= 3)
+ rails (7.2.2)
+ actioncable (= 7.2.2)
+ actionmailbox (= 7.2.2)
+ actionmailer (= 7.2.2)
+ actionpack (= 7.2.2)
+ actiontext (= 7.2.2)
+ actionview (= 7.2.2)
+ activejob (= 7.2.2)
+ activemodel (= 7.2.2)
+ activerecord (= 7.2.2)
+ activestorage (= 7.2.2)
+ activesupport (= 7.2.2)
bundler (>= 1.15.0)
- railties (= 7.1.3.4)
+ railties (= 7.2.2)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -522,13 +523,13 @@ GEM
activesupport (>= 4.2)
choice (~> 0.2.0)
ruby-graphviz (~> 1.2)
- rails-html-sanitizer (1.6.0)
+ rails-html-sanitizer (1.6.2)
loofah (~> 2.21)
- nokogiri (~> 1.14)
- railties (7.1.3.4)
- actionpack (= 7.1.3.4)
- activesupport (= 7.1.3.4)
- irb
+ nokogiri (>= 1.15.7, != 1.16.7, != 1.16.6, != 1.16.5, != 1.16.4, != 1.16.3, != 1.16.2, != 1.16.1, != 1.16.0.rc1, != 1.16.0)
+ railties (7.2.2)
+ actionpack (= 7.2.2)
+ activesupport (= 7.2.2)
+ irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
thor (~> 1.0, >= 1.2.2)
@@ -538,17 +539,17 @@ GEM
rb-fsevent (0.11.2)
rb-inotify (0.10.1)
ffi (~> 1.0)
- rdoc (6.7.0)
+ rdoc (6.10.0)
psych (>= 4.0.0)
recaptcha (5.17.0)
redis (5.3.0)
redis-client (>= 0.22.0)
redis-client (0.22.2)
connection_pool
- regexp_parser (2.9.2)
- reline (0.5.10)
+ regexp_parser (2.9.3)
+ reline (0.6.0)
io-console (~> 0.5)
- request_store (1.5.1)
+ request_store (1.7.0)
rack (>= 1.4)
responders (3.1.1)
actionpack (>= 5.2)
@@ -577,20 +578,19 @@ GEM
rspec-mocks (~> 3.13)
rspec-support (~> 3.13)
rspec-support (3.13.1)
- rubocop (1.65.1)
+ rubocop (1.69.2)
json (~> 2.3)
language_server-protocol (>= 3.17.0)
parallel (~> 1.10)
parser (>= 3.3.0.2)
rainbow (>= 2.2.2, < 4.0)
- regexp_parser (>= 2.4, < 3.0)
- rexml (>= 3.2.5, < 4.0)
- rubocop-ast (>= 1.31.1, < 2.0)
+ regexp_parser (>= 2.9.3, < 3.0)
+ rubocop-ast (>= 1.36.2, < 2.0)
ruby-progressbar (~> 1.7)
- unicode-display_width (>= 2.4.0, < 3.0)
- rubocop-ast (1.33.0)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.37.0)
parser (>= 3.3.1.0)
- rubocop-performance (1.22.1)
+ rubocop-performance (1.23.0)
rubocop (>= 1.48.1, < 2.0)
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rails (2.25.1)
@@ -619,6 +619,7 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
+ securerandom (0.4.1)
shellany (0.0.1)
shoulda-matchers (6.2.0)
activesupport (>= 5.2.0)
@@ -631,10 +632,11 @@ GEM
simplecov_json_formatter (~> 0.1)
simplecov-html (0.12.3)
simplecov_json_formatter (0.1.4)
- sinatra (3.1.0)
+ sinatra (4.0.0)
mustermann (~> 3.0)
- rack (~> 2.2, >= 2.2.4)
- rack-protection (= 3.1.0)
+ rack (>= 3.0.0, < 4)
+ rack-protection (= 4.0.0)
+ rack-session (>= 2.0.0, < 3)
tilt (~> 2.0)
slop (3.6.0)
smart_properties (1.17.0)
@@ -648,36 +650,39 @@ GEM
actionpack (>= 5.2)
activesupport (>= 5.2)
sprockets (>= 3.0.0)
- standard (1.40.0)
+ standard (1.43.0)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.0)
- rubocop (~> 1.65.0)
+ rubocop (~> 1.69.1)
standard-custom (~> 1.0.0)
- standard-performance (~> 1.4)
+ standard-performance (~> 1.6)
standard-custom (1.0.2)
lint_roller (~> 1.0)
rubocop (~> 1.50)
- standard-performance (1.5.0)
+ standard-performance (1.6.0)
lint_roller (~> 1.1)
- rubocop-performance (~> 1.22.0)
+ rubocop-performance (~> 1.23.0)
stimulus-rails (1.3.4)
railties (>= 6.0.0)
- stringio (3.1.1)
+ stringio (3.1.2)
strong_migrations (1.8.0)
activerecord (>= 5.2)
terser (1.2.4)
execjs (>= 0.3.0, < 3)
thor (1.3.2)
tilt (2.2.0)
- timeout (0.4.2)
+ timeout (0.4.1)
ttfunk (1.7.0)
turbo-rails (2.0.10)
actionpack (>= 6.0.0)
railties (>= 6.0.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
- unicode-display_width (2.6.0)
+ unicode-display_width (3.1.2)
+ unicode-emoji (~> 4.0, >= 4.0.4)
+ unicode-emoji (4.0.4)
uniform_notifier (1.16.0)
+ useragent (0.16.10)
version_gem (1.1.4)
warden (1.2.9)
rack (>= 2.0.9)
@@ -690,21 +695,20 @@ GEM
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
- webrick (1.8.2)
+ webrick (1.9.1)
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
xpath (3.2.0)
nokogiri (~> 1.8)
yard (0.9.34)
- zeitwerk (2.6.18)
+ zeitwerk (2.7.1)
PLATFORMS
arm64-darwin-20
arm64-darwin-21
arm64-darwin-22
arm64-darwin-23
- arm64-darwin-24
x86_64-darwin-20
x86_64-darwin-21
x86_64-darwin-22
@@ -717,6 +721,7 @@ DEPENDENCIES
azure-storage-blob
better_errors
binding_of_caller
+ bootsnap
bootstrap (~> 5.2)
brakeman
bugsnag
@@ -775,7 +780,7 @@ DEPENDENCIES
pry-rails
pry-remote
puma
- rails (= 7.1.3.4)
+ rails (= 7.2.2)
rails-controller-testing
rails-erd
recaptcha
@@ -799,4 +804,4 @@ DEPENDENCIES
webmock (~> 3.24)
BUNDLED WITH
- 2.5.23
+ 2.5.22
diff --git a/app/controllers/audits_controller.rb b/app/controllers/audits_controller.rb
index 5cb0f22d8b..8d68291cee 100644
--- a/app/controllers/audits_controller.rb
+++ b/app/controllers/audits_controller.rb
@@ -10,7 +10,7 @@ def index
end
def show
- @items = View::Inventory.items_for_location(@audit.storage_location)
+ @items = View::Inventory.items_for_location(@audit.storage_location, include_omitted: true)
end
def edit
@@ -93,7 +93,7 @@ def set_storage_locations
end
def set_items
- @items = current_organization.items.alphabetized
+ @items = current_organization.items.where(active: true).alphabetized
end
def save_audit_status_and_redirect(params)
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index c1af5332b2..1bc266c6bd 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -4,9 +4,13 @@ class DashboardController < ApplicationController
def index
@org_stats = OrganizationStats.new(current_organization)
- @total_inventory = current_organization.total_inventory
@partners_awaiting_review = current_organization.partners.awaiting_review
- @outstanding_requests = current_organization.ordered_requests.where(status: %i[pending started]).order(:created_at)
+ @outstanding_requests = current_organization
+ .ordered_requests
+ .includes(:partner_user, :partner)
+ .where(status: %i[pending started])
+ .order(:created_at)
+ .limit(25)
@low_inventory_report = LowInventoryQuery.call(current_organization)
diff --git a/app/controllers/distributions_by_county_controller.rb b/app/controllers/distributions_by_county_controller.rb
index 715dc7256d..d1ca1ee9e7 100644
--- a/app/controllers/distributions_by_county_controller.rb
+++ b/app/controllers/distributions_by_county_controller.rb
@@ -4,7 +4,12 @@ class DistributionsByCountyController < ApplicationController
def report
setup_date_range_picker
- distributions = current_organization.distributions.includes(:partner).during(helpers.selected_range)
- @breakdown = DistributionByCountyReportService.new.get_breakdown(distributions)
+ start_date = helpers.selected_range.first.iso8601
+ end_date = helpers.selected_range.last.iso8601
+ @breakdown = DistributionSummaryByCountyQuery.call(
+ organization_id: current_organization.id,
+ start_date: start_date,
+ end_date: end_date
+ )
end
end
diff --git a/app/controllers/distributions_controller.rb b/app/controllers/distributions_controller.rb
index 3ff9d70e32..ef0cc2e113 100644
--- a/app/controllers/distributions_controller.rb
+++ b/app/controllers/distributions_controller.rb
@@ -50,7 +50,7 @@ def index
@items = current_organization.items.alphabetized.select(:id, :name)
@item_categories = current_organization.item_categories.select(:id, :name)
@storage_locations = current_organization.storage_locations.active_locations.alphabetized.select(:id, :name)
- @partners = Partner.joins(:distributions).where(distributions: @distributions).distinct.order(:name).select(:id, :name)
+ @partners = current_organization.partners.active.alphabetized.select(:id, :name)
@selected_item = filter_params[:by_item_id].presence
@distribution_totals = DistributionTotalsService.new(current_organization.distributions, scope_filters)
@total_value_all_distributions = @distribution_totals.total_value
@@ -117,7 +117,7 @@ def create
elsif request_id
@distribution.initialize_request_items
end
- @items = current_organization.items.alphabetized
+ @items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized
inventory = View::Inventory.new(@distribution.organization_id)
@@ -152,7 +152,7 @@ def new
@distribution.line_items.build
@distribution.copy_from_donation(params[:donation_id], params[:storage_location_id])
end
- @items = current_organization.items.alphabetized
+ @items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.where.not(status: 'deactivated').alphabetized
inventory = View::Inventory.new(current_organization.id)
@@ -178,7 +178,7 @@ def edit
if (!@distribution.complete? && @distribution.future?) ||
current_user.has_role?(Role::ORG_ADMIN, current_organization)
@distribution.line_items.build if @distribution.line_items.size.zero?
- @items = current_organization.items.alphabetized
+ @items = current_organization.items.active.alphabetized
@partner_list = current_organization.partners.alphabetized
@audit_warning = current_organization.audits
.where(storage_location_id: @distribution.storage_location_id)
@@ -209,7 +209,8 @@ def update
flash[:error] = insufficient_error_message(result.error.message)
@distribution.line_items.build if @distribution.line_items.size.zero?
@distribution.initialize_request_items
- @items = current_organization.items.alphabetized
+ @items = current_organization.items.active.alphabetized
+ @partner_list = current_organization.partners.alphabetized
@storage_locations = current_organization.storage_locations.active_locations.alphabetized
render :edit
end
diff --git a/app/controllers/partners/family_requests_controller.rb b/app/controllers/partners/family_requests_controller.rb
index 6f4f94b34c..f1ba5f0f3c 100644
--- a/app/controllers/partners/family_requests_controller.rb
+++ b/app/controllers/partners/family_requests_controller.rb
@@ -19,7 +19,7 @@ def create
create_service = Partners::FamilyRequestCreateService.new(
partner_user_id: current_user.id,
family_requests_attributes: family_requests_attributes,
- for_families: true
+ request_type: "child"
)
create_service.call
@@ -37,7 +37,7 @@ def validate
@partner_request = Partners::FamilyRequestCreateService.new(
partner_user_id: current_user.id,
family_requests_attributes: family_requests_attributes,
- for_families: true
+ request_type: "child"
).initialize_only
if @partner_request.valid?
@total_items = @partner_request.total_items
diff --git a/app/controllers/partners/individuals_requests_controller.rb b/app/controllers/partners/individuals_requests_controller.rb
index 9432bdc16f..bd85254e04 100644
--- a/app/controllers/partners/individuals_requests_controller.rb
+++ b/app/controllers/partners/individuals_requests_controller.rb
@@ -12,7 +12,8 @@ def create
create_service = Partners::FamilyRequestCreateService.new(
partner_user_id: current_user.id,
comments: individuals_request_params[:comments],
- family_requests_attributes: individuals_request_params[:items_attributes]&.values
+ family_requests_attributes: individuals_request_params[:items_attributes]&.values,
+ request_type: "individual"
)
create_service.call
@@ -36,7 +37,8 @@ def validate
@partner_request = Partners::FamilyRequestCreateService.new(
partner_user_id: current_user.id,
comments: individuals_request_params[:comments],
- family_requests_attributes: individuals_request_params[:items_attributes]&.values
+ family_requests_attributes: individuals_request_params[:items_attributes]&.values,
+ request_type: "individual"
).initialize_only
if @partner_request.valid?
@total_items = @partner_request.total_items
diff --git a/app/models/account_request.rb b/app/models/account_request.rb
index c68fe75559..f28194996c 100644
--- a/app/models/account_request.rb
+++ b/app/models/account_request.rb
@@ -30,7 +30,7 @@ class AccountRequest < ApplicationRecord
has_one :organization, dependent: :nullify
- enum status: %w[started user_confirmed admin_approved rejected admin_closed].map { |v| [v, v] }.to_h
+ enum :status, %w[started user_confirmed admin_approved rejected admin_closed].map { |v| [v, v] }.to_h
scope :requested, -> { where(status: %w[started user_confirmed]) }
scope :closed, -> { where(status: %w[admin_approved rejected admin_closed]) }
diff --git a/app/models/barcode_item.rb b/app/models/barcode_item.rb
index 9e9141322c..cb05987b43 100644
--- a/app/models/barcode_item.rb
+++ b/app/models/barcode_item.rb
@@ -49,8 +49,11 @@ class BarcodeItem < ApplicationRecord
scope :global, -> { where(barcodeable_type: "BaseItem") }
- alias_attribute :item, :barcodeable
- alias_attribute :base_item, :barcodeable
+ # aliases of barcodeable
+ belongs_to :item, polymorphic: true, dependent: :destroy,
+ counter_cache: :barcode_count, foreign_key: :barcodeable_id, foreign_type: :barcodeable_type
+ belongs_to :base_item, polymorphic: true, dependent: :destroy,
+ counter_cache: :barcode_count, foreign_key: :barcodeable_id, foreign_type: :barcodeable_type
def to_h
{
diff --git a/app/models/broadcast_announcement.rb b/app/models/broadcast_announcement.rb
index e1818f572f..af74b21d60 100644
--- a/app/models/broadcast_announcement.rb
+++ b/app/models/broadcast_announcement.rb
@@ -25,7 +25,7 @@ def expired?
def self.filter_announcements(parent_org)
BroadcastAnnouncement.where(organization_id: parent_org)
- .where("expiry IS ? or expiry >= ?", nil, Time.zone.today)
+ .where("expiry IS NULL or expiry >= ?", Time.zone.today)
.order(created_at: :desc)
end
end
diff --git a/app/models/concerns/issued_at.rb b/app/models/concerns/issued_at.rb
index 3abfa79c53..b28261b96f 100644
--- a/app/models/concerns/issued_at.rb
+++ b/app/models/concerns/issued_at.rb
@@ -5,23 +5,18 @@ module IssuedAt
extend ActiveSupport::Concern
included do
- before_create :initialize_issued_at
- before_save :initialize_issued_at
scope :by_issued_at, ->(issued_at) { where(issued_at: issued_at.beginning_of_month..issued_at.end_of_month) }
scope :for_year, ->(year) { where("extract(year from issued_at) = ?", year) }
+ validates :issued_at, presence: true
validate :issued_at_cannot_be_before_2000
validate :issued_at_cannot_be_further_than_1_year
end
private
- def initialize_issued_at
- self.issued_at ||= created_at&.end_of_day
- end
-
def issued_at_cannot_be_before_2000
if issued_at.present? && issued_at < Date.new(2000, 1, 1)
- errors.add(:issued_at, "Cannot be before 2000")
+ errors.add(:issued_at, "cannot be before 2000")
end
end
diff --git a/app/models/distribution.rb b/app/models/distribution.rb
index 1d11c25d7b..1f7d10c857 100644
--- a/app/models/distribution.rb
+++ b/app/models/distribution.rb
@@ -44,9 +44,11 @@ class Distribution < ApplicationRecord
before_save :combine_distribution, :reset_shipping_cost
- enum state: { scheduled: 5, complete: 10 }
- enum delivery_method: { pick_up: 0, delivery: 1, shipped: 2 }
+ enum :state, { scheduled: 5, complete: 10 }
+ enum :delivery_method, { pick_up: 0, delivery: 1, shipped: 2 }
scope :active, -> { joins(:line_items).joins(:items).where(items: { active: true }) }
+ scope :with_diapers, -> { joins(line_items: :item).merge(Item.disposable.or(Item.cloth_diapers)) }
+ scope :with_period_supplies, -> { joins(line_items: :item).merge(Item.period_supplies) }
# add item_id scope to allow filtering distributions by item
scope :by_item_id, ->(item_id) { includes(:items).where(items: { id: item_id }) }
# partner scope to allow filtering by partner
@@ -71,6 +73,10 @@ class Distribution < ApplicationRecord
where("issued_at > :start_date AND issued_at <= :end_date",
start_date: Time.zone.today.beginning_of_week.beginning_of_day, end_date: Time.zone.today.end_of_week.end_of_day)
end
+ scope :in_last_12_months, -> do
+ where("issued_at > :start_date AND issued_at <= :end_date",
+ start_date: 12.months.ago.beginning_of_day, end_date: Time.zone.today.end_of_day)
+ end
delegate :name, to: :partner, prefix: true
diff --git a/app/models/item.rb b/app/models/item.rb
index 8f6b1eaec1..688bd34512 100644
--- a/app/models/item.rb
+++ b/app/models/item.rb
@@ -143,7 +143,7 @@ def is_in_kit?(kits = nil)
end
def can_delete?(inventory = nil, kits = nil)
- can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request?
+ can_deactivate_or_delete?(inventory, kits) && line_items.none? && !barcode_count&.positive? && !in_request? && kit.blank?
end
# @return [Boolean]
diff --git a/app/models/partner.rb b/app/models/partner.rb
index 2e7b5ab77b..858d01a67e 100644
--- a/app/models/partner.rb
+++ b/app/models/partner.rb
@@ -27,7 +27,7 @@ class Partner < ApplicationRecord
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
].freeze
- enum status: { uninvited: 0, invited: 1, awaiting_review: 2, approved: 3, error: 4, recertification_required: 5, deactivated: 6 }
+ enum :status, { uninvited: 0, invited: 1, awaiting_review: 2, approved: 3, error: 4, recertification_required: 5, deactivated: 6 }
belongs_to :organization
belongs_to :partner_group, optional: true
@@ -74,6 +74,7 @@ class Partner < ApplicationRecord
AGENCY_TYPES = {
"CAREER" => "Career technical training",
"ABUSE" => "Child abuse resource center",
+ "BNB" => "Basic Needs Bank",
"CHURCH" => "Church outreach ministry",
"COLLEGE" => "College and Universities",
"CDC" => "Community development corporation",
@@ -82,6 +83,7 @@ class Partner < ApplicationRecord
"LEGAL" => "Correctional Facilities / Jail / Prison / Legal System",
"CRISIS" => "Crisis/Disaster services",
"DISAB" => "Developmental disabilities program",
+ "DISTRICT" => "School District",
"DOMV" => "Domestic violence shelter",
"ECE" => "Early Childhood Education/Childcare",
"CHILD" => "Early childhood services",
@@ -96,6 +98,7 @@ class Partner < ApplicationRecord
"HOSP" => "Hospital",
"INFPAN" => "Infant/Child Pantry/Closet",
"LIB" => "Library",
+ "MHEALTH" => "Mental Health",
"MILITARY" => "Military Bases/Veteran Services",
"POLICE" => "Police Station",
"PREG" => "Pregnancy resource center",
@@ -185,7 +188,10 @@ def self.csv_export_headers
"Contact Name",
"Contact Phone",
"Contact Email",
- "Notes"
+ "Notes",
+ "Counties Served",
+ "Providing Diapers",
+ "Providing Period Supplies"
]
end
@@ -202,10 +208,21 @@ def csv_export_attributes
contact_person[:name],
contact_person[:phone],
contact_person[:email],
- notes
+ notes,
+ profile.county_list_by_region,
+ providing_diapers,
+ providing_period_supplies
]
end
+ def providing_diapers
+ distributions.in_last_12_months.with_diapers.any? ? "Y" : "N"
+ end
+
+ def providing_period_supplies
+ distributions.in_last_12_months.with_period_supplies.any? ? "Y" : "N"
+ end
+
def contact_person
return @contact_person if @contact_person
diff --git a/app/models/partners/profile.rb b/app/models/partners/profile.rb
index 3fd9fcb397..271ec146b5 100644
--- a/app/models/partners/profile.rb
+++ b/app/models/partners/profile.rb
@@ -91,6 +91,7 @@ class Profile < Base
has_many :served_areas, foreign_key: "partner_profile_id", class_name: "Partners::ServedArea", dependent: :destroy, inverse_of: :partner_profile
+ has_many :counties, through: :served_areas
accepts_nested_attributes_for :served_areas, allow_destroy: true
has_many_attached :documents
@@ -125,6 +126,11 @@ def split_pick_up_emails
pick_up_email.split(/,|\s+/).compact_blank
end
+ def county_list_by_region
+ # provides a county list in case insensitive alpha order, by region, then county name
+ counties.order(%w(lower(region) lower(name))).pluck(:name).join("; ")
+ end
+
private
def check_social_media
diff --git a/app/models/request.rb b/app/models/request.rb
index 0c2647ccc1..7e78fb0133 100644
--- a/app/models/request.rb
+++ b/app/models/request.rb
@@ -31,8 +31,8 @@ class Request < ApplicationRecord
accepts_nested_attributes_for :item_requests, allow_destroy: true, reject_if: proc { |attributes| attributes["quantity"].blank? }
has_many :child_item_requests, through: :item_requests
- enum status: { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, _prefix: true
- enum request_type: %w[quantity individual child].map { |v| [v, v] }.to_h
+ enum :status, { pending: 0, started: 1, fulfilled: 2, discarded: 3 }, prefix: true
+ enum :request_type, %w[quantity individual child].map { |v| [v, v] }.to_h
validates :distribution_id, uniqueness: true, allow_nil: true
validate :item_requests_uniqueness_by_item_id
diff --git a/app/models/transfer.rb b/app/models/transfer.rb
index 4237f5fef0..49b2248f2a 100644
--- a/app/models/transfer.rb
+++ b/app/models/transfer.rb
@@ -20,7 +20,8 @@ class Transfer < ApplicationRecord
include Itemizable
include Filterable
include Exportable
- alias_attribute :storage_location, :from # to make it play nice with Itemizable
+ # to make it play nice with Itemizable - alias of `from`
+ belongs_to :storage_location, class_name: "StorageLocation", inverse_of: :transfers_from, foreign_key: :from_id
scope :from_location, ->(location_id) { where(from_id: location_id) }
scope :to_location, ->(location_id) { where(to_id: location_id) }
scope :for_csv_export, ->(organization, *) {
diff --git a/app/pdfs/distribution_pdf.rb b/app/pdfs/distribution_pdf.rb
index 5942348593..896d5b48d5 100644
--- a/app/pdfs/distribution_pdf.rb
+++ b/app/pdfs/distribution_pdf.rb
@@ -162,8 +162,6 @@ def request_data
"Value/item",
"In-Kind Value Received",
"Packages"]]
-
- inventory = nil
inventory = View::Inventory.new(@distribution.organization_id)
request_items = @distribution.request.request_items.map do |request_item|
RequestItem.from_json(request_item, @distribution.request, inventory)
diff --git a/app/pdfs/donation_pdf.rb b/app/pdfs/donation_pdf.rb
index 773e1b9095..bbf2a19eb0 100644
--- a/app/pdfs/donation_pdf.rb
+++ b/app/pdfs/donation_pdf.rb
@@ -20,9 +20,13 @@ def initialize(donation)
@address = nil
@email = nil
when Donation::SOURCES[:product_drive]
- @name = donation.product_drive_participant.business_name
- @address = donation.product_drive_participant.address
- @email = donation.product_drive_participant.email
+ if donation.product_drive_participant
+ @name = donation.product_drive_participant.business_name
+ @address = donation.product_drive_participant.address
+ @email = donation.product_drive_participant.email
+ else
+ @name = "Product Drive -- #{donation.product_drive.name}"
+ end
when Donation::SOURCES[:misc]
@name = "Misc. Donation"
@address = nil
diff --git a/app/queries/distribution_summary_by_county_query.rb b/app/queries/distribution_summary_by_county_query.rb
new file mode 100644
index 0000000000..afe9dc157d
--- /dev/null
+++ b/app/queries/distribution_summary_by_county_query.rb
@@ -0,0 +1,90 @@
+class DistributionSummaryByCountyQuery
+ CountySummary = Data.define(:name, :quantity, :value)
+
+ # No need to send comments in the query
+ SQL_MULTILINE_COMMENTS = /\/\*.*?\*\//
+
+ DISTRIBUTION_BY_COUNTY_SQL = <<~SQL.squish.gsub(SQL_MULTILINE_COMMENTS, "").freeze
+ /* Calculate total item quantity and value per distribution */
+ WITH distribution_totals AS
+ (
+ SELECT DISTINCT d.id,
+ d.partner_id,
+ COALESCE(SUM(li.quantity) OVER (PARTITION BY d.id), 0) AS quantity,
+ COALESCE(SUM(COALESCE(i.value_in_cents, 0) * li.quantity) OVER (PARTITION BY d.id), 0) AS value
+ FROM distributions d
+ JOIN line_items li ON li.itemizable_id = d.id AND li.itemizable_type = 'Distribution'
+ JOIN items i ON i.id = li.item_id
+ WHERE d.issued_at BETWEEN :start_date AND :end_date
+ AND d.organization_id = :organization_id
+ GROUP BY d.id, li.id, i.id
+ ),
+ /* Match distribution totals with client share and counties.
+ If distribution has no associated county, set county name to "Unspecified"
+ and set region to ZZZ so it will be last when sorted */
+ totals_by_county AS
+ (
+ SELECT dt.id,
+ dt.quantity,
+ dt.value,
+ COALESCE(psa.client_share::float / 100, 1) AS percentage,
+ COALESCE(c.name, 'Unspecified') county_name,
+ COALESCE(c.region, 'ZZZ') county_region
+ FROM distribution_totals dt
+ LEFT JOIN partners p ON p.id = dt.partner_id
+ LEFT JOIN partner_profiles pp ON pp.partner_id = p.id
+ LEFT JOIN partner_served_areas psa ON psa.partner_profile_id = pp.id
+ LEFT JOIN counties c ON c.id = psa.county_id
+ UNION
+ /* Previous behavior was to add a row for unspecified counties
+ even if all distributions have an associated county */
+ SELECT 0 AS id,
+ 0 AS quantity,
+ 0 AS value,
+ 1 AS percentage,
+ 'Unspecified' AS county_name,
+ 'ZZZ' AS county_region
+ )
+ /* Distribution value and quantities per county share may not be whole numbers,
+ so we cast to an integer for rounding purposes */
+ SELECT tbc.county_name AS name,
+ SUM((tbc.quantity * percentage)::int) AS quantity,
+ SUM((tbc.value * percentage)::int) AS value
+ FROM totals_by_county tbc
+ GROUP BY county_name, county_region
+ ORDER BY county_region ASC;
+ SQL
+
+ class << self
+ def call(organization_id:, start_date: nil, end_date: nil)
+ params = {
+ organization_id: organization_id,
+ start_date: start_date || "1000-01-01",
+ end_date: end_date || "3000-01-01"
+ }
+
+ execute(to_sql(DISTRIBUTION_BY_COUNTY_SQL, **params)).to_a.map(&to_county_summary)
+ end
+
+ private
+
+ def execute(sql)
+ ActiveRecord::Base.connection.execute(sql)
+ end
+
+ def to_sql(query, organization_id:, start_date:, end_date:)
+ ActiveRecord::Base.sanitize_sql_array(
+ [
+ query,
+ organization_id: organization_id,
+ start_date: start_date,
+ end_date: end_date
+ ]
+ )
+ end
+
+ def to_county_summary
+ ->(params) { CountySummary.new(**params) }
+ end
+ end
+end
diff --git a/app/services/distribution_by_county_report_service.rb b/app/services/distribution_by_county_report_service.rb
deleted file mode 100644
index c20d263923..0000000000
--- a/app/services/distribution_by_county_report_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-# frozen_string_literal: true
-
-class DistributionByCountyReportService
- Breakdown = Struct.new(:name, :region, :num_items, :amount)
- def get_breakdown(distributions)
- breakdowns = {}
- breakdowns["Unspecified"] = Breakdown.new("Unspecified", "ZZZ", 0, 0.00)
- distributions.each do |distribution|
- served_areas = distribution.partner.profile.served_areas
- num_items_for_distribution = distribution.line_items.total
- value_of_distribution = distribution.line_items.total_value
- if served_areas.size == 0
- breakdowns["Unspecified"].num_items += num_items_for_distribution
- breakdowns["Unspecified"].amount += value_of_distribution
- else
- served_areas.each do |served_area|
- name = served_area.county.name
- percentage = served_area.client_share / 100.0
- if !breakdowns[name]
- breakdowns[name] = Breakdown.new(name, served_area.county.region,
- (num_items_for_distribution * percentage).round(0), value_of_distribution * percentage)
- else
- breakdowns[name].num_items = breakdowns[name].num_items + (num_items_for_distribution * percentage).round(0)
- breakdowns[name].amount = breakdowns[name].amount + value_of_distribution * percentage
- end
- end
- end
- end
-
- breakdown_array = breakdowns.sort_by { |k, v| [v.region, k] }
- @breakdown = breakdown_array.map { |a| a[1] }
- end
-end
diff --git a/app/services/partners/family_request_create_service.rb b/app/services/partners/family_request_create_service.rb
index 8e018530db..c84da76a8a 100644
--- a/app/services/partners/family_request_create_service.rb
+++ b/app/services/partners/family_request_create_service.rb
@@ -6,13 +6,13 @@ module Partners
class FamilyRequestCreateService
include ServiceObjectErrorsMixin
- attr_reader :partner_user_id, :comments, :family_requests_attributes, :partner_request
+ attr_reader :partner_user_id, :comments, :family_requests_attributes, :partner_request, :request_type
- def initialize(partner_user_id:, family_requests_attributes:, comments: nil, for_families: false)
+ def initialize(partner_user_id:, family_requests_attributes:, request_type:, comments: nil)
@partner_user_id = partner_user_id
@comments = comments
@family_requests_attributes = family_requests_attributes.presence || []
- @for_families = for_families
+ @request_type = request_type
end
def call
@@ -81,9 +81,5 @@ def convert_person_count_to_item_quantity(item_id:, person_count:)
def included_items_by_id
@included_items_by_id ||= Item.where(id: family_requests_attributes.pluck(:item_id)).index_by(&:id)
end
-
- def request_type
- @for_families ? "child" : "individual"
- end
end
end
diff --git a/app/views/audits/_form.html.erb b/app/views/audits/_form.html.erb
index d9014cae5f..8cb798a588 100644
--- a/app/views/audits/_form.html.erb
+++ b/app/views/audits/_form.html.erb
@@ -14,7 +14,7 @@
<%= simple_form_for @audit, data: { controller: "form-input" }, html: {class: "storage-location-required"} do |f| %>
- <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?" } %>
+ <%= render partial: "storage_locations/source", object: f, locals: { label: "Storage location", error: "What storage location are you auditing?", include_omitted_items: true } %>