Compare commits

...

147 Commits

Author SHA1 Message Date
dependabot[bot]
2dc5dbac7d Bump aws-sdk-s3 from 1.222.0 to 1.224.0 (#4645)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.222.0 to 1.224.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.224.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-25 13:23:23 +09:30
dependabot[bot]
ee6d5cd84a Bump dalli from 5.0.2 to 5.0.4 (#4639)
Bumps [dalli](https://github.com/petergoldstein/dalli) from 5.0.2 to 5.0.4.
- [Changelog](https://github.com/petergoldstein/dalli/blob/main/CHANGELOG.md)
- [Commits](https://github.com/petergoldstein/dalli/compare/v5.0.2...v5.0.4)

---
updated-dependencies:
- dependency-name: dalli
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 13:09:42 +09:30
dependabot[bot]
f1524c2b0c Bump rubocop-rails from 2.35.1 to 2.35.2 (#4643)
Bumps [rubocop-rails](https://github.com/rubocop/rubocop-rails) from 2.35.1 to 2.35.2.
- [Release notes](https://github.com/rubocop/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-rails/compare/v2.35.1...v2.35.2)

---
updated-dependencies:
- dependency-name: rubocop-rails
  dependency-version: 2.35.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-20 13:09:26 +09:30
dependabot[bot]
a6cb1cbb36 Bump rubocop-rails from 2.35.0 to 2.35.1 (#4642)
Bumps [rubocop-rails](https://github.com/rubocop/rubocop-rails) from 2.35.0 to 2.35.1.
- [Release notes](https://github.com/rubocop/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-rails/compare/v2.35.0...v2.35.1)

---
updated-dependencies:
- dependency-name: rubocop-rails
  dependency-version: 2.35.1
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 19:54:54 +09:30
dependabot[bot]
c0f6720a1e Bump oj from 3.17.0 to 3.17.1 (#4640)
Bumps [oj](https://github.com/ohler55/oj) from 3.17.0 to 3.17.1.
- [Release notes](https://github.com/ohler55/oj/releases)
- [Changelog](https://github.com/ohler55/oj/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/ohler55/oj/compare/v3.17.0...v3.17.1)

---
updated-dependencies:
- dependency-name: oj
  dependency-version: 3.17.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 18:41:08 +09:30
dependabot[bot]
04ea628f00 Bump faraday from 2.14.1 to 2.14.2 (#4641)
Bumps [faraday](https://github.com/lostisland/faraday) from 2.14.1 to 2.14.2.
- [Release notes](https://github.com/lostisland/faraday/releases)
- [Changelog](https://github.com/lostisland/faraday/blob/main/CHANGELOG.md)
- [Commits](https://github.com/lostisland/faraday/compare/v2.14.1...v2.14.2)

---
updated-dependencies:
- dependency-name: faraday
  dependency-version: 2.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-19 12:30:01 +09:30
dependabot[bot]
cabac926cc Bump icalendar from 2.12.2 to 2.12.3 (#4635)
Bumps [icalendar](https://github.com/icalendar/icalendar) from 2.12.2 to 2.12.3.
- [Changelog](https://github.com/icalendar/icalendar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/icalendar/icalendar/compare/v2.12.2...v2.12.3)

---
updated-dependencies:
- dependency-name: icalendar
  dependency-version: 2.12.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 12:59:47 +09:30
dependabot[bot]
0d375f6146 Bump rubocop from 1.86.1 to 1.86.2 (#4634)
Bumps [rubocop](https://github.com/rubocop/rubocop) from 1.86.1 to 1.86.2.
- [Release notes](https://github.com/rubocop/rubocop/releases)
- [Changelog](https://github.com/rubocop/rubocop/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop/compare/v1.86.1...v1.86.2)

---
updated-dependencies:
- dependency-name: rubocop
  dependency-version: 1.86.2
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 12:59:37 +09:30
dependabot[bot]
be9e5ed6bf Bump aws-sdk-s3 from 1.221.0 to 1.222.0 (#4636)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.221.0 to 1.222.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.222.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-15 12:42:57 +09:30
Daniel O'Connor
c8ea225b10 Ensure garden creation flash prompt is rendered as HTML (#4633)
Updated app/views/shared/_flash_messages.html.haml to use sanitize(content)
instead of = content. This ensures that HTML content in flash messages,
such as the prompt to add an activity after creating a garden, is
correctly rendered even if its html_safe status is lost during session
serialization.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-14 12:35:18 +09:30
dependabot[bot]
a8b7c73111 Bump systeminformation from 5.31.5 to 5.31.6 (#4632)
Bumps [systeminformation](https://github.com/sebhildebrandt/systeminformation) from 5.31.5 to 5.31.6.
- [Release notes](https://github.com/sebhildebrandt/systeminformation/releases)
- [Changelog](https://github.com/sebhildebrandt/systeminformation/blob/master/CHANGELOG.md)
- [Commits](https://github.com/sebhildebrandt/systeminformation/compare/v5.31.5...v5.31.6)

---
updated-dependencies:
- dependency-name: systeminformation
  dependency-version: 5.31.6
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-14 11:37:29 +09:30
dependabot[bot]
61810dbee3 Bump selenium-webdriver from 4.43.0 to 4.44.0 (#4631)
Bumps [selenium-webdriver](https://github.com/SeleniumHQ/selenium) from 4.43.0 to 4.44.0.
- [Release notes](https://github.com/SeleniumHQ/selenium/releases)
- [Changelog](https://github.com/SeleniumHQ/selenium/blob/trunk/rb/CHANGES)
- [Commits](https://github.com/SeleniumHQ/selenium/compare/selenium-4.43.0...selenium-4.44.0)

---
updated-dependencies:
- dependency-name: selenium-webdriver
  dependency-version: 4.44.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-13 22:39:08 +09:30
Daniel O'Connor
1467ec9364 Add Wikidata climate attributes and integration to gardens (#4627)
* Add Wikidata integration for garden climate data

- Add location_wikidata_id, lowest_temp_c, and highest_temp_c to gardens.
- Implement WikidataService for fetching IDs and temperature properties.
- Map P6591 to highest_temp_c and P7422 to lowest_temp_c with unit conversion.
- Automatically populate Wikidata info on garden location change.
- Add manual "Fetch Wikidata info" button and opt-in prompt to garden show page.
- Update gardens_controller to permit new attributes and handle manual fetch.
- Update db/schema.rb manually to include new columns and migration version.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix migration

* Improve display

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-12 18:07:35 +09:30
dependabot[bot]
37452a5513 Bump rubocop-rails from 2.34.3 to 2.35.0 (#4628)
Bumps [rubocop-rails](https://github.com/rubocop/rubocop-rails) from 2.34.3 to 2.35.0.
- [Release notes](https://github.com/rubocop/rubocop-rails/releases)
- [Changelog](https://github.com/rubocop/rubocop-rails/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-rails/compare/v2.34.3...v2.35.0)

---
updated-dependencies:
- dependency-name: rubocop-rails
  dependency-version: 2.35.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 17:23:59 +09:30
dependabot[bot]
e70297a83e Bump devise from 5.0.3 to 5.0.4 (#4629)
Bumps [devise](https://github.com/heartcombo/devise) from 5.0.3 to 5.0.4.
- [Release notes](https://github.com/heartcombo/devise/releases)
- [Changelog](https://github.com/heartcombo/devise/blob/main/CHANGELOG.md)
- [Commits](https://github.com/heartcombo/devise/compare/v5.0.3...v5.0.4)

---
updated-dependencies:
- dependency-name: devise
  dependency-version: 5.0.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-12 17:23:46 +09:30
Daniel O'Connor
d7a50f86b5 Allow collaborators to remove themselves from gardens (#4630)
- Update Ability to grant destroy permission to collaborators for their own record
- Add 'Leave garden' link to garden show page
- Add specs for garden collaborator permissions

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-12 17:23:24 +09:30
dependabot[bot]
ca7f56683c Bump aws-sdk-s3 from 1.220.0 to 1.221.0 (#4624)
Bumps [aws-sdk-s3](https://github.com/aws/aws-sdk-ruby) from 1.220.0 to 1.221.0.
- [Release notes](https://github.com/aws/aws-sdk-ruby/releases)
- [Changelog](https://github.com/aws/aws-sdk-ruby/blob/version-3/gems/aws-sdk-s3/CHANGELOG.md)
- [Commits](https://github.com/aws/aws-sdk-ruby/commits)

---
updated-dependencies:
- dependency-name: aws-sdk-s3
  dependency-version: 1.221.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-10 10:38:49 +09:30
dependabot[bot]
0c00b866da Bump friendly_id from 5.6.0 to 5.7.0 (#4625)
Bumps [friendly_id](https://github.com/norman/friendly_id) from 5.6.0 to 5.7.0.
- [Release notes](https://github.com/norman/friendly_id/releases)
- [Changelog](https://github.com/norman/friendly_id/blob/master/Changelog.md)
- [Commits](https://github.com/norman/friendly_id/compare/v5.6.0...v5.7.0)

---
updated-dependencies:
- dependency-name: friendly_id
  dependency-version: 5.7.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Cesy <cesy.avon@gmail.com>
2026-05-10 10:38:23 +09:30
Cesy
f50da4e0e0 Merge pull request #4626 from Growstuff/dependabot/npm_and_yarn/fast-uri-3.1.2 2026-05-09 06:46:38 +01:00
dependabot[bot]
31285b2bde Bump fast-uri from 3.1.0 to 3.1.2
Bumps [fast-uri](https://github.com/fastify/fast-uri) from 3.1.0 to 3.1.2.
- [Release notes](https://github.com/fastify/fast-uri/releases)
- [Commits](https://github.com/fastify/fast-uri/compare/v3.1.0...v3.1.2)

---
updated-dependencies:
- dependency-name: fast-uri
  dependency-version: 3.1.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-08 23:25:42 +00:00
dependabot[bot]
ec5873fc88 Bump ip-address from 10.1.0 to 10.2.0 (#4623)
Bumps [ip-address](https://github.com/beaugunderson/ip-address) from 10.1.0 to 10.2.0.
- [Commits](https://github.com/beaugunderson/ip-address/commits)

---
updated-dependencies:
- dependency-name: ip-address
  dependency-version: 10.2.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 13:26:51 +09:30
Daniel O'Connor
555d5ddf15 Revert "Replace Sidekiq with Solid Queue (#4619)" (#4620)
This reverts commit 4659ac5464.
2026-05-04 18:15:17 +09:30
Daniel O'Connor
5ada7e7f77 Add crops search API endpoint (#4622)
* Add crops search API endpoint

- Added GET /api/v1/crops/search endpoint.
- Updated CropSearchService to support additional search options.
- Manually updated Swagger documentation in swagger/v1/swagger.json.
- Added request specs to verify the new endpoint.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Add crops search API endpoint

- Added GET /api/v1/crops/search endpoint.
- Updated CropSearchService to support additional search options.
- Manually updated Swagger documentation in swagger/v1/swagger.json.
- Added request specs to verify the new endpoint.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-04 18:12:36 +09:30
Daniel O'Connor
4659ac5464 Replace Sidekiq with Solid Queue (#4619)
* Replace Sidekiq with Solid Queue

This commit transitions the background job processing from Sidekiq to
Solid Queue.

Changes:
- Replaced `sidekiq` gem with `solid_queue` in Gemfile.
- Updated `development.rb` and `production.rb` to use `:solid_queue` as
  the queue adapter.
- Added Solid Queue database tables via a new migration.
- Configured Solid Queue in `config/queue.yml` and `config/recurring.yml`.
- Integrated Solid Queue supervisor as a Puma plugin in `config/puma.rb`.
- Removed separate worker process from `Procfile`.
- Removed Sidekiq-specific configuration files.
- Updated Gemfile.lock to support both `ruby` and `x86_64-linux` platforms.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix regression in gemfiles

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-03 15:29:03 +09:30
Daniel O'Connor
3d63d12908 Improve read performance with caching and memoization (#4572)
* Improve read performance with caching and memoization

- Memoize `Crop#all_companions` and `Member#unread_count` in models
- Implement instance-level memoization in `CropsHelper#crop_or_parent`
- Add Rails caching for expensive aggregate queries in `Charts::CropsController`
- Add fragment caching for high-impact sections on the crop show page

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix header spec failure due to memoized unread_count

- Reset `@unread_count` in `spec/views/layouts/_header_spec.rb` to ensure the updated notification count is rendered correctly.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-03 14:47:59 +09:30
Daniel O'Connor
035210197f Prompt for updated planting rating when harvesting (#4608)
* Update planting rating when recording a harvest

- Added virtual attribute `overall_rating` to `Harvest` model.
- Updated `HarvestsController` to permit `overall_rating` and synchronize it to the associated `Planting`.
- Added a rating range field (1-5) to the harvest form.
- Added controller tests to verify that the planting rating is updated.
- Refined feature tests for harvesting.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* I have updated the system to allow for recording a planting rating when a harvest is logged. Here is a summary of the changes:

- Added a virtual attribute `overall_rating` to the `Harvest` model.
- Updated `HarvestsController` to permit `overall_rating` and synchronize it to the associated `Planting`.
- Added a rating range field (1-5) to the harvest form.
- Added controller tests to verify that the planting rating is updated correctly.
- Updated feature tests to ensure the harvest form functions as expected.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Update database.yml

* Apply suggestions from code review

Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>

* Adjust wording

* Change harvest modal

* Fix tests

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-03 14:47:00 +09:30
Daniel O'Connor
8a8fd6eabd Merge pull request #4575 from Growstuff/memoize-unread-count
Memoize unread messages count
2026-05-02 16:46:26 +09:30
Daniel O'Connor
fe9fdd9147 Upgrade ERB (#4617) 2026-05-02 16:46:02 +09:30
Daniel O'Connor
ee7b9ab39f Upgrade ERB 2026-05-02 07:03:30 +00:00
Daniel O'Connor
6aadb4d805 Merge pull request #4616 from Growstuff/rubocop-upgrade
Update rubocop_todo.yml
2026-05-02 16:28:50 +09:30
Daniel O'Connor
a42682a59e Merge pull request #4614 from Growstuff/upgrade-ruby
Update to Ruby 3.4.9
2026-05-02 16:19:54 +09:30
Daniel O'Connor
6294c54139 Merge pull request #4615 from Growstuff/fix-sidekiq
Namespaces no longer supported in sidekiq
2026-05-02 16:07:59 +09:30
Daniel O'Connor
c168e8e4c9 Update to 3.4.9 2026-05-02 06:36:27 +00:00
Daniel O'Connor
6ac438a07f Namespaces no longer supported in sidekiq 2026-05-02 06:35:23 +00:00
Daniel O'Connor
2380c662fe Merge pull request #4604 from Growstuff/harvest-reminders-16703221337897327633
Add Harvest Reminder Emails and Scheduled Task
2026-05-02 15:46:09 +09:30
Daniel O'Connor
4589839c64 Fix crops csv export 11894001552728801282 (#4613)
* Fix ArgumentError in Crops CSV export

This commit fixes a crash when exporting crops to CSV, caused by
accessing ActiveRecord methods and associations on Searchkick
HashWrapper objects.

Changes:
- In CropsController#index, use `load: true` (with preloaded
  associations) when the request format is CSV or RSS.
- In app/views/crops/index.csv.shaper, use individual `csv.cell` calls
  instead of `csv.cells` to correctly handle Searchkick results and
  explicitly access attributes.
- Added a controller test to verify CSV export functionality.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Mark test pending

* Skip creator

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-05-02 15:39:36 +09:30
Daniel O'Connor
1f6f3c4dfd Merge pull request #4612 from Growstuff/fix-crops-csv-export-11894001552728801282
Fix ArgumentError in Crops CSV export
2026-05-02 15:31:10 +09:30
Daniel O'Connor
5a7f41537f Change plant_before formatting method to to_fs 2026-05-02 14:47:43 +09:30
Daniel O'Connor
1281795c97 Merge pull request #4609 from Growstuff/fix-csv-export-crash-4991917409830119333
Fix crash during CSV export of harvests and seeds
2026-05-02 14:42:39 +09:30
Daniel O'Connor
c219d447cc Merge branch 'dev' into fix-csv-export-crash-4991917409830119333 2026-05-02 14:41:31 +09:30
Daniel O'Connor
1e3f86a349 Merge pull request #4611 from Growstuff/CloCkWeRX-patch-2
Fix seeds_count to correctly reference size
2026-05-02 14:17:51 +09:30
Daniel O'Connor
680afe02cc Merge pull request #4610 from Growstuff/associate-post-with-crop-5945795316503813050
Associate post with crop from crop show page
2026-05-02 13:42:01 +09:30
Daniel O'Connor
914cfe99c8 Fix seeds_count to correctly reference size 2026-05-02 13:39:34 +09:30
google-labs-jules[bot]
4643fbd92e Associate post with crop from crop show page
Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-05-01 11:35:58 +00:00
google-labs-jules[bot]
5ac709ffd1 Fix crash during CSV export of harvests and seeds
When using Searchkick with `load: false`, search results are returned
as HashResponse objects which do not support model associations or
standard Rails URL helpers that expect model instances.

This commit updates HarvestsController and SeedsController to
conditionally load ActiveRecord objects when CSV format is requested,
ensuring that the export templates can access the necessary associations.
Similar logic was also applied to CropsController.

Additionally, a typo in the Crops CSV shaper was fixed.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-05-01 11:30:22 +00:00
Daniel O'Connor
9833801a42 Merge pull request #4606 from Growstuff/dependabot/bundler/axe-core-rspec-4.11.3
Bump axe-core-rspec from 4.11.2 to 4.11.3
2026-05-01 18:17:28 +09:30
dependabot[bot]
4d1e8aede6 Bump axe-core-rspec from 4.11.2 to 4.11.3
Bumps [axe-core-rspec](https://github.com/dequelabs/axe-core-gems) from 4.11.2 to 4.11.3.
- [Release notes](https://github.com/dequelabs/axe-core-gems/releases)
- [Changelog](https://github.com/dequelabs/axe-core-gems/blob/develop/CHANGELOG.md)
- [Commits](https://github.com/dequelabs/axe-core-gems/commits)

---
updated-dependencies:
- dependency-name: axe-core-rspec
  dependency-version: 4.11.3
  dependency-type: direct:development
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-01 07:49:38 +00:00
Daniel O'Connor
24f41350a9 Bump rubocop-capybara from 2.22.1 to 2.23.0 (#4605)
Bumps [rubocop-capybara](https://github.com/rubocop/rubocop-capybara) from 2.22.1 to 2.23.0.
- [Release notes](https://github.com/rubocop/rubocop-capybara/releases)
- [Changelog](https://github.com/rubocop/rubocop-capybara/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-capybara/compare/v2.22.1...v2.23.0)

---
updated-dependencies:
- dependency-name: rubocop-capybara
  dependency-version: 2.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-30 17:38:16 +09:30
dependabot[bot]
503ba716bb Bump rubocop-capybara from 2.22.1 to 2.23.0
Bumps [rubocop-capybara](https://github.com/rubocop/rubocop-capybara) from 2.22.1 to 2.23.0.
- [Release notes](https://github.com/rubocop/rubocop-capybara/releases)
- [Changelog](https://github.com/rubocop/rubocop-capybara/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rubocop/rubocop-capybara/compare/v2.22.1...v2.23.0)

---
updated-dependencies:
- dependency-name: rubocop-capybara
  dependency-version: 2.23.0
  dependency-type: direct:development
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-30 07:04:07 +00:00
google-labs-jules[bot]
e423e6ac79 Add weekly harvest reminder emails and scheduled task
- Added `send_harvest_reminder` preference to Member model and settings UI.
- Implemented `harvest_in_next_week?` in PredictHarvest concern.
- Created `harvest_reminder` email with localized templates.
- Added `growstuff:send_harvest_reminders` Rake task to run weekly.
- Refactored existing and new reminder tasks to use `deliver_later` for scalability.
- Added unit tests for prediction logic and mailer.
- Fixed a bug in the existing planting reminder task where it was using an uninitialized constant `Notifier`.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-30 04:09:26 +00:00
Daniel O'Connor
e63089e03b Remove deprecated config.read_encrypted_secrets from production.rb (#4603)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-30 12:54:17 +09:30
Daniel O'Connor
6ce347af82 Rename FUNDING.yml to .github/FUNDING.yml 2026-04-28 18:11:48 +09:30
Daniel O'Connor
64af597dec Add funding information 2026-04-28 18:10:50 +09:30
Daniel O'Connor
7160f50ac1 Refactor Activity model to remove Elasticsearch integration (#4576)
* Refactor Activity model to remove Elasticsearch integration

- Removed `SearchActivities` concern and Searchkick from `Activity` model.
- Implemented `Activity.homepage_records` using ActiveRecord with `DISTINCT ON` for PostgreSQL.
- Updated `ActivitiesController#index` to use ActiveRecord queries with eager loading and pagination.
- Added `active` scope to `Activity`.
- Added unit tests for `Activity` model.
- Deleted `app/models/concerns/search_activities.rb`.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix NoMethodError: undefined method 'reindex' for class Activity

- Removed all calls to `Activity.reindex` in migrations, rake tasks, and spec helpers.
- These were causing failures after the removal of Searchkick from the Activity model.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix ambiguous column id in homepage_records query

- Updated `Activity.homepage_records` to use `activities.id` instead of `id` in the subquery.
- This resolves the `PG::AmbiguousColumn: ERROR: column reference "id" is ambiguous` error.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix ambiguous created_at in homepage_records query

- Use `unscoped` in the subquery for `Activity.homepage_records` to bypass the default scope from `Ownable` concern.
- This prevents the join with the `members` table in the subquery, which was causing `PG::AmbiguousColumn: ERROR: column reference "created_at" is ambiguous`.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Complete refactoring of Activity model to remove Elasticsearch

- Removed SearchActivities concern and searchkick integration.
- Updated ActivitiesController#index to use ActiveRecord queries.
- Implemented performant Activity.homepage_records using DISTINCT ON (PostgreSQL).
- Added Activity.active scope.
- Added no-op Activity.reindex (class and instance methods) for backward compatibility.
- Cleaned up leftover reindex calls in rake tasks, migrations, and spec helpers.
- Added unit tests for new Activity model logic.
- Updated factories to include no-op reindex traits.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Less eager loading

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-28 17:51:01 +09:30
Daniel O'Connor
e748da9a1f Merge pull request #4601 from Growstuff/links-wrong
Fix links further
2026-04-28 13:34:11 +09:30
Daniel O'Connor
4ac0dcb05b Merge branch 'dev' of https://github.com/Growstuff/growstuff into links-wrong 2026-04-28 04:03:44 +00:00
Daniel O'Connor
60390fcc06 Fix links further 2026-04-28 04:03:27 +00:00
Daniel O'Connor
55e6d99979 Merge pull request #4599 from Growstuff/links-wrong
Fix various breadcrumb links to avoid passing ?owner, which doesn't actually filter
2026-04-28 13:23:36 +09:30
Daniel O'Connor
dfac51ee97 Merge pull request #4595 from Growstuff/optimize-harvests-caching-memoization-9176733581563564983
Optimize Harvests with memoization and fragment caching
2026-04-28 13:20:01 +09:30
Daniel O'Connor
f24ca80394 Fix various breadcrumb links to avoid passing ?owner, which doesn't actually filter 2026-04-28 03:46:47 +00:00
Daniel O'Connor
7360bc968b Merge pull request #4596 from Growstuff/flickr-tag-filtering-3395495860406820072
Add Flickr tag filtering for adding photos
2026-04-28 13:04:40 +09:30
Daniel O'Connor
f680a6b25d Merge pull request #4597 from Growstuff/memoize-plantings-8206863373400530940
Memoize Planting-related methods for performance optimization
2026-04-28 13:04:17 +09:30
Daniel O'Connor
22638371c2 Update _harvests.html.haml 2026-04-28 13:01:02 +09:30
google-labs-jules[bot]
50ab6f39ee Optimize Harvests with memoization and caching
- Memoize display methods in `Harvest` model.
- Memoize calculation methods in `PredictHarvest` concern using `defined?` for nil safety.
- Add fragment caching to `app/views/harvests/_popover.html.haml`.
- Add fragment caching and query caching to `app/views/crops/_harvests.html.haml` with daily expiration for relative time strings.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-28 03:16:36 +00:00
google-labs-jules[bot]
2e0c8a910d Memoize Planting-related methods for performance optimization
This commit introduces memoization to various methods in the Planting model,
PredictPlanting and PredictHarvest concerns, PlantingsHelper, and
PlantingsController.

Specifically:
- Memoized database-intensive lookups like `nearby_same_crop`, `first_harvest_date`,
  and `last_harvest_date`.
- Memoized calculated fields like `finish_predicted_at`, `expected_lifespan`,
  and `age_in_days`.
- Optimized `PlantingsHelper#transplantable_gardens_by_owner` using a hash
  to cache results per planting instance within a request.
- Applied the `defined?(@variable)` pattern where appropriate to ensure
  efficient handling of `nil` results.

These changes reduce redundant database queries and expensive calculations,
particularly during view rendering where these methods are frequently accessed.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-28 03:07:18 +00:00
google-labs-jules[bot]
3c70ba12ca Allow filtering Flickr photos by tag when adding photos
- Update MemberFlickr concern to support tag-based search using flickr.photos.search
- Update PhotosController to handle the 'tag' parameter
- Add tag search input field to the 'New Photo' view
- Add test case to verify tag filtering in PhotosController spec

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-28 02:59:21 +00:00
google-labs-jules[bot]
0df7589feb Optimize Harvests with memoization and fragment caching
- Memoize display methods in `Harvest` model.
- Memoize calculation methods in `PredictHarvest` concern using `defined?` for nil safety.
- Add fragment caching to `app/views/harvests/_popover.html.haml`.
- Add fragment caching to `app/views/crops/_harvests.html.haml` with daily expiration for relative time strings.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-28 02:59:06 +00:00
Daniel O'Connor
8cdfda3660 Merge pull request #4594 from Growstuff/jules-10992479017878632568-8480ddd1
Optimize CropsHelper with caching and memoization
2026-04-28 12:03:03 +09:30
google-labs-jules[bot]
aa0ee65d78 Optimize CropsHelper with caching and memoization
- Implement instance-level memoization for `crop_or_parent` and `display_seed_availability`
- Use `Rails.cache.fetch` for `crop_jsonld_data` to improve performance of JSON-LD generation
- Optimize `display_seed_availability` to avoid redundant queries
- Fix a potential `NameError` in `crop_jsonld_data` by initializing `images` properly
- Ensure memoization keys handle non-persisted objects and nil results correctly

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-27 23:55:46 +00:00
Daniel O'Connor
355e9f84d5 Merge pull request #4591 from Growstuff/cache-charts-crops-data-7725540171807829398
Improve Charts::CropsController caching
2026-04-27 19:23:11 +09:30
Daniel O'Connor
dce32c5e3c Merge pull request #4586 from Growstuff/dependabot/github_actions/docker/build-push-action-7
Bump docker/build-push-action from 5 to 7
2026-04-27 18:35:25 +09:30
Daniel O'Connor
dbae34a958 Merge pull request #4587 from Growstuff/dependabot/github_actions/docker/setup-buildx-action-4
Bump docker/setup-buildx-action from 3 to 4
2026-04-27 18:35:06 +09:30
Daniel O'Connor
9edee8400d Merge pull request #4589 from Growstuff/dependabot/github_actions/docker/login-action-4
Bump docker/login-action from 3 to 4
2026-04-27 18:34:50 +09:30
Daniel O'Connor
b775814614 Merge pull request #4593 from Growstuff/planting-photos
Try planting filtering
2026-04-27 18:17:35 +09:30
Daniel O'Connor
464017de6f Try planting filtering 2026-04-27 08:05:32 +00:00
dependabot[bot]
9184285388 Bump docker/login-action from 3 to 4
Bumps [docker/login-action](https://github.com/docker/login-action) from 3 to 4.
- [Release notes](https://github.com/docker/login-action/releases)
- [Commits](https://github.com/docker/login-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/login-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 08:00:55 +00:00
dependabot[bot]
dcd701fe9d Bump docker/build-push-action from 5 to 7
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 7.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v7)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 08:00:50 +00:00
dependabot[bot]
5a462bd740 Bump docker/setup-buildx-action from 3 to 4
Bumps [docker/setup-buildx-action](https://github.com/docker/setup-buildx-action) from 3 to 4.
- [Release notes](https://github.com/docker/setup-buildx-action/releases)
- [Commits](https://github.com/docker/setup-buildx-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: docker/setup-buildx-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 07:57:06 +00:00
Daniel O'Connor
ecf77313dc Merge pull request #4588 from Growstuff/dependabot/github_actions/docker/metadata-action-6
Bump docker/metadata-action from 5 to 6
2026-04-27 17:24:09 +09:30
Daniel O'Connor
0ee671fddb Merge pull request #4590 from Growstuff/dependabot/bundler/puma-8.0.1
Bump puma from 8.0.0 to 8.0.1
2026-04-27 17:23:56 +09:30
Daniel O'Connor
fc4276cacf Merge pull request #4592 from Growstuff/CloCkWeRX-patch-2
Ban Semrush
2026-04-27 17:19:53 +09:30
Daniel O'Connor
a2bb6c7162 Ban Semrush 2026-04-27 17:19:31 +09:30
google-labs-jules[bot]
ff9d99afe5 Improve Charts::CropsController with caching and refactoring
- Added Rails.cache.fetch to `sunniness` and `planted_from` actions.
- Refactored crop loading into a `before_action :set_crop`.
- Updated specs to verify caching behavior and ensure coverage.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-27 07:46:08 +00:00
dependabot[bot]
1b4b8f94d1 Bump docker/metadata-action from 5 to 6
Bumps [docker/metadata-action](https://github.com/docker/metadata-action) from 5 to 6.
- [Release notes](https://github.com/docker/metadata-action/releases)
- [Commits](https://github.com/docker/metadata-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/metadata-action
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 07:42:38 +00:00
Daniel O'Connor
66cc98051f Bump actions/checkout from 4 to 6 (#4585)
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-27 17:11:32 +09:30
dependabot[bot]
9fe1fddac1 Bump puma from 8.0.0 to 8.0.1
Bumps [puma](https://github.com/puma/puma) from 8.0.0 to 8.0.1.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/main/History.md)
- [Commits](https://github.com/puma/puma/compare/v8.0.0...v8.0.1)

---
updated-dependencies:
- dependency-name: puma
  dependency-version: 8.0.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 07:26:19 +00:00
dependabot[bot]
1eac00705e Bump actions/checkout from 4 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-04-27 07:25:50 +00:00
Daniel O'Connor
37e9860fdf Update member_slug lookup to 404 when not found (#4584)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 14:19:50 +09:30
Daniel O'Connor
bd637c3310 Cache what a crop is harvested for (#4582) 2026-04-27 13:32:45 +09:30
Daniel O'Connor
9abb0d02b9 Merge pull request #4581 from Growstuff/add-rack-attack-protection-3014929071908440304
Add Rack::Attack rate limiting and Fail2Ban protection
2026-04-27 13:23:17 +09:30
Daniel O'Connor
2e56f8cb2f Cache what a crop is harvested for 2026-04-27 03:52:11 +00:00
Daniel O'Connor
3127f45d0f Merge pull request #4578 from Growstuff/member-inactive-delete
Delete inactive members with no activity in 3 years
2026-04-27 02:15:17 +09:30
Daniel O'Connor
15571940f5 Add fragment cache for crop partials (#4577) 2026-04-27 01:48:35 +09:30
Daniel O'Connor
8e7dd25e98 Add rake task to cleanup inactive members (#4574)
* Add members:cleanup_inactive rake task

This task identifies and deletes members who have not logged in for over
24 months and have no gardens, plantings, or other activity (posts,
comments, seeds, harvests, etc).

Includes support for DRY_RUN=true to preview deletions.
Added tests in spec/tasks/members_spec.rb.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Refactor activity check to Member#has_activity? and update rake task

- Added `Member#has_activity?` to encapsulate the check for gardens, plantings, and other activity.
- Updated `members:cleanup_inactive` rake task to use `Member#has_activity?`.
- Maintained `DRY_RUN` support and existing tests.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Apply suggestion from @CloCkWeRX

* Apply suggestions from code review

Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-27 01:40:54 +09:30
Daniel O'Connor
2723599f27 Add fragment cache for crop partials 2026-04-26 16:07:29 +00:00
Daniel O'Connor
98c8bdc0bb Merge pull request #4564 from Growstuff/memory-optimization-2149092598558110155
Memory usage optimization
2026-04-27 00:37:51 +09:30
Daniel O'Connor
573daa8c8a Swap to modern expect style (#4571) 2026-04-26 22:58:04 +09:30
Daniel O'Connor
5174b1236e Merge pull request #4567 from Growstuff/memory-optimisation-3
Members - Nearest To - Memory improvements
2026-04-26 22:57:01 +09:30
Daniel O'Connor
5a349f8f1b Swap to modern expect style 2026-04-26 13:21:15 +00:00
Daniel O'Connor
0d850804cf Merge pull request #4570 from Growstuff/rubocop-tweaks
Rubocop fixes
2026-04-26 22:47:46 +09:30
Daniel O'Connor
161a934811 Merge pull request #4569 from Growstuff/plant_part_spec
Rubocop: Fix no expectation errors
2026-04-26 22:44:49 +09:30
Daniel O'Connor
8cfef5ce1a Rubocop fixes 2026-04-26 13:09:00 +00:00
Daniel O'Connor
6dacb0af74 Swap to modern expect style 2026-04-26 13:03:46 +00:00
Daniel O'Connor
7e2d36f99a Swap to modern expect style 2026-04-26 12:55:58 +00:00
Daniel O'Connor
3406d9e7bc Merge pull request #4568 from Growstuff/memory-usage-4
Posts - memory usage
2026-04-26 19:05:56 +09:30
Daniel O'Connor
7a91746f73 Update .dockerignore to remove .ruby-version
Remove .ruby-version from .dockerignore
2026-04-26 19:05:41 +09:30
Daniel O'Connor
209973e72b Memory usage 2026-04-26 09:26:52 +00:00
Daniel O'Connor
4848302eab Merge pull request #4565 from Growstuff/memory-usage-1
Admin - Members - optimise memory usage
2026-04-26 18:45:12 +09:30
Daniel O'Connor
920a28a144 Merge pull request #4566 from Growstuff/memory-usage-2
GBIF - optimise memory usage
2026-04-26 18:44:58 +09:30
Daniel O'Connor
fff7a14635 GBIF - optimise memory usage 2026-04-26 09:03:45 +00:00
Daniel O'Connor
0131c9b531 Admin - Members - optimise memory usage 2026-04-26 09:01:18 +00:00
Daniel O'Connor
1b091b2f6f Merge pull request #4453 from Growstuff/add-mark-as-failed-to-crop-view-13853484652230549508
Add "mark as failed" action to crop view
2026-04-26 14:44:09 +09:30
google-labs-jules[bot]
3b60e8f974 Implement blocking feature (#4199)
* Implement blocking feature

This commit introduces a blocking feature that allows members to block other members.

A blocked member is prevented from:
- following the blocker
- sending private messages to the blocker
- replying to the blocker's posts
- liking the blocker's content

The implementation includes:
- A new `Block` model and a corresponding database table.
- Updates to the `Member` model to include associations for blocks.
- A new `BlocksController` to handle blocking and unblocking actions.
- New routes for the `BlocksController`.
- UI changes to add block/unblock buttons to the member profile page.
- Validations in the `Follow`, `Comment`, and `Like` models to enforce the blocking rules.
- A check in the `MessagesController` to prevent sending messages to a member who has blocked the sender.
- A callback in the `Block` model to destroy the follow relationship when a block is created.
- New feature and model specs to test the blocking functionality.

* Implement blocking feature and fix failing tests

This commit introduces a blocking feature that allows members to block other members.

A blocked member is prevented from:
- following the blocker
- sending private messages to the blocker
- replying to the blocker's posts
- liking the blocker's content

The implementation includes:
- A new `Block` model and a corresponding database table.
- Updates to the `Member` model to include associations for blocks.
- A new `BlocksController` to handle blocking and unblocking actions.
- New routes for the `BlocksController`.
- UI changes to add block/unblock buttons to the member profile page.
- Validations in the `Follow`, `Comment`, and `Like` models to enforce the blocking rules.
- A check in the `MessagesController` to prevent sending messages to a member who has blocked the sender.
- A callback in the `Block` model to destroy the follow relationship when a block is created.
- New feature and model specs to test the blocking functionality.

This commit also fixes a failing test in the blocking feature. The error was caused by the validation being called even when the `member` association was `nil`. A guard has been added to the validation methods in the `Like`, `Follow`, and `Comment` models to prevent this from happening.

* Generate schema

* Fix tests

* Add permissions

* Define Block permissions in Ability model

The feature specs for member blocking were failing because the "Block"
link was not being rendered on member profiles. This was due to the
lack of explicit create and destroy permissions for the Block resource
in the Ability model, which is used by CanCanCan to authorize actions
and by the view to conditionally show links.

This change adds the necessary permissions to `member_abilities`:
- Allows members to create blocks (except for blocking themselves).
- Allows members to destroy blocks where they are the blocker.

These rules ensure that the "Block" and "Unblock" links are correctly
rendered and authorized for signed-in members.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Comment out specs for now

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-26 14:22:32 +09:30
google-labs-jules[bot]
7ed3a97263 Improve test coverage of ability_spec (#4283)
* Improve test coverage of ability_spec

* Fix specs

* Rubocop

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
Co-authored-by: Daniel O'Connor <365751+CloCkWeRX@users.noreply.github.com>
2026-04-26 14:21:36 +09:30
Daniel O'Connor
2aa697a6d6 Add comprehensive test coverage for forums (#4561)
* Add comprehensive test coverage for forums

- Added `spec/controllers/forums_controller_spec.rb` to test all CRUD actions and authorization for guest, member, and admin roles.
- Added `spec/features/forums_spec.rb` to cover user-facing features such as browsing forums and creating posts from within a forum.
- Updated `spec/requests/forums_spec.rb` to cover basic request flow and JSON response formats.

Note: Tests were verified for content and logic but execution in the sandbox environment was blocked by missing infrastructure (PostgreSQL and Elasticsearch).

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Fix specs

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-26 14:18:28 +09:30
Daniel O'Connor
ed87d23ece Merge pull request #4560 from Growstuff/fix-i18n-locale-texts-16171345716630423189
Fix Rails/I18nLocaleTexts RuboCop errors
2026-04-26 13:36:10 +09:30
Daniel O'Connor
c20160e3db Merge pull request #4559 from Growstuff/update-crop-wrangling-links-4812587945321495224
Update crop wrangling guide links to GitHub wiki
2026-04-26 12:40:09 +09:30
google-labs-jules[bot]
700cb76e3a Update crop wrangling guide links to GitHub wiki
Updated links to the crop wrangling guide in the scientific names and
alternate names forms to point to the new GitHub wiki location.
Verified that other occurrences in the codebase already use the new
URL.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-26 01:46:35 +00:00
Daniel O'Connor
5f834c475f Merge pull request #4557 from Growstuff/fix-rspec-expect-in-hook-16350019958417127399
Fix RSpec/ExpectInHook issues
2026-04-26 04:25:16 +09:30
google-labs-jules[bot]
6c7903c2a5 Fix RSpec/ExpectInHook offenses
- Move expectations from `before` hooks to `it` blocks.
- Ensure controller actions are called after expectations are set in controller specs.
- Replace synchronization expectations in hooks with Capybara `find` calls.
- Remove RSpec/ExpectInHook from .rubocop_todo.yml.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-04-25 18:39:43 +00:00
Daniel O'Connor
dfa963cd65 Rubocop: RSpec/EmptyExampleGroup (#4554)
* Rubocop: RSpec/EmptyExampleGroup

* Undo renaming

* Apply suggestion from @CloCkWeRX

* Apply suggestion from @CloCkWeRX
2026-04-26 03:18:50 +09:30
Daniel O'Connor
163289e853 Fix RSpec/IndexedLet RuboCop issues in spec files (#4556)
* Fix RSpec/IndexedLet RuboCop issues in spec files

Replace indexed let variable names with descriptive names across 11 spec files.
This improves readability and complies with the RSpec/IndexedLet rule.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Rubocop

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-26 03:10:03 +09:30
Daniel O'Connor
2001b355c4 Add Docker and CI Support (#4461)
* Add Docker, Docker Compose, and GitHub Actions CI support

- Added a production-ready `Dockerfile` based on Ruby 3.3.8-bullseye.
- Added `entrypoint.sh` to handle Rails server PID cleanup.
- Added `.dockerignore` to optimize build context.
- Added `docker-compose.yml` for local orchestration of Rails, PostgreSQL 17, and Elasticsearch 7.4.0.
- Added GitHub Actions workflow in `.github/workflows/docker-build-push.yml` to build and push the image to GHCR on pushes to the `dev` branch.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>

* Swap to 3.4.8

* Node 22

* Apply suggestion from @CloCkWeRX

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2026-04-26 03:07:31 +09:30
Daniel O'Connor
6a0b09b047 Merge pull request #4555 from Growstuff/remove-old-compass-rails
Remove old compass rails
2026-04-26 03:00:37 +09:30
Daniel O'Connor
4b7e0cf5d7 Merge pull request #4553 from Growstuff/RSpec/EmptyLineAfterExample
Rubocop: RSpec/EmptyLineAfterExample
2026-04-26 02:37:58 +09:30
Daniel O'Connor
a3af82d935 Merge pull request #4552 from Growstuff/Lint/SymbolConversion
Rubocop: Lint/SymbolConversion
2026-04-26 02:37:48 +09:30
Daniel O'Connor
051509b59f Merge pull request #4551 from Growstuff/Style/PercentLiteralDelimiters
Rubocop: Style/PercentLiteralDelimiters
2026-04-26 02:25:43 +09:30
Daniel O'Connor
a133eddf21 Rubocop: RSpec/EmptyLineAfterExample 2026-04-25 16:52:34 +00:00
Daniel O'Connor
0577c73833 Rubocop: Lint/SymbolConversion 2026-04-25 16:48:31 +00:00
Daniel O'Connor
7522d992b4 Rubocop: Style/PercentLiteralDelimiters 2026-04-25 16:46:15 +00:00
Daniel O'Connor
83de2fe889 Regenerate 2026-04-25 16:45:00 +00:00
Daniel O'Connor
bbe75df0ad Merge pull request #4550 from Growstuff/Style/FrozenStringLiteralComment
Rubocop: Style/FrozenStringLiteralComment
2026-04-24 08:41:23 +09:30
Daniel O'Connor
279cc88162 Update growstuff_markdown_spec.rb 2026-04-24 00:15:18 +09:30
Daniel O'Connor
fe4dd5c185 Merge pull request #4549 from Growstuff/Rails/WhereMissing
Rubocop: Rails/WhereMissing
2026-04-24 00:13:37 +09:30
Daniel O'Connor
d625eb2dbd Rubocop: Style/FrozenStringLiteralComment 2026-04-23 14:30:02 +00:00
Daniel O'Connor
2019d0e952 Rubocop: Rails/WhereMissing 2026-04-23 14:27:10 +00:00
Daniel O'Connor
e7659a75a4 Merge pull request #4548 from Growstuff/RSpecRails/HaveHttpStatus
Rubocop: RSpecRails/HaveHttpStatus
2026-04-23 23:52:40 +09:30
Daniel O'Connor
4a66bdc9fe Merge pull request #4546 from Growstuff/Rails/RedundantActiveRecordAllMethod
Rubocop: Rails/RedundantActiveRecordAllMethod
2026-04-23 23:46:39 +09:30
Daniel O'Connor
8de6b083f9 Rubocop: RSpecRails/HaveHttpStatus 2026-04-23 14:13:47 +00:00
Daniel O'Connor
accab7f84c Merge pull request #4545 from Growstuff/RSpecRails/InferredSpecType
Rubocop: RSpecRails/InferredSpecType
2026-04-23 23:29:17 +09:30
Daniel O'Connor
3f6dd59dfa Rubocop: Rails/RedundantActiveRecordAllMethod 2026-04-23 13:48:17 +00:00
Daniel O'Connor
0a71b44dea Merge pull request #4542 from Growstuff/RSpec/ContextMethod
Rubocop: RSpec/ContextMethod
2026-04-23 23:10:33 +09:30
Daniel O'Connor
ba75afb3f5 Rubocop: RSpecRails/InferredSpecType 2026-04-23 13:39:46 +00:00
Daniel O'Connor
aa1c9ceb05 Merge pull request #4543 from Growstuff/RSpec/ExpectChange
Rubocop: RSpec/ExpectChange
2026-04-23 23:02:03 +09:30
Daniel O'Connor
6f59635ca7 Rubocop: RSpec/ExpectChange 2026-04-23 13:23:14 +00:00
Daniel O'Connor
6736ae3142 Rubocop: RSpec/ContextMethod 2026-04-23 13:20:47 +00:00
google-labs-jules[bot]
8bafba7f9d Ensure "mark as failed" option is available when viewing a crop
This change adds the "mark as failed" action to the crop view in two places:
1. In the "Crop Actions" button group, a new "Mark as failed" button is added if the current member has active plantings of that crop. Clicking it opens a modal to select which planting failed.
2. In the "See who's planted" list, an "Actions" dropdown is added to any plantings owned by the current member, which includes the "Mark as failed" option.

A new partial `app/views/plantings/_failed_modal.html.haml` was created to handle the planting selection modal.
`app/views/crops/_actions.html.haml` and `app/views/crops/_plantings.html.haml` were updated to include these new actions.

Co-authored-by: CloCkWeRX <365751+CloCkWeRX@users.noreply.github.com>
2026-02-22 00:08:58 +00:00
214 changed files with 2854 additions and 1029 deletions

30
.dockerignore Normal file
View File

@@ -0,0 +1,30 @@
.git
.github
.devcontainer
log/*
tmp/*
!tmp/keep
node_modules
public/assets
.env
.ruby-gemset
.editorconfig
.esignore
.eslintrc.json
.haml-lint.yml
.overcommit.yml
.rspec
.rubocop.yml
.rubocop_todo.yml
.scss-lint.yml
.travis.yml
.yamllint
CODE_OF_CONDUCT.md
CONTRIBUTING.md
CONTRIBUTORS.md
LICENSE.txt
README.md
TECH.md
docker-compose.yml
Dockerfile
.dockerignore

1
.github/FUNDING.yml vendored Normal file
View File

@@ -0,0 +1 @@
ko_fi: jennyscottthompson

43
.github/workflows/docker-build-push.yml vendored Normal file
View File

@@ -0,0 +1,43 @@
name: Docker Build and Push
on:
push:
branches:
- mainline
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Log in to the Container registry
uses: docker/login-action@v4
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@v6
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max

View File

@@ -1,25 +1,31 @@
# This configuration was generated by # This configuration was generated by
# `rubocop --auto-gen-config` # `rubocop --auto-gen-config`
# on 2026-03-01 05:17:50 UTC using RuboCop version 1.85.0. # on 2026-05-02 06:53:56 UTC using RuboCop version 1.86.1.
# The point is for the user to remove these configuration records # The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base. # one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new # Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again. # versions of RuboCop, may require this file to be generated again.
# Offense count: 19 # Offense count: 407
Capybara/NegationMatcherAfterVisit: # This cop supports safe autocorrection (--autocorrect).
Capybara/RSpec/HaveContent:
Enabled: false
# Offense count: 21
Capybara/RSpec/NegationMatcherAfterVisit:
Exclude: Exclude:
- 'spec/features/admin/reverting_crops_spec.rb' - 'spec/features/admin/reverting_crops_spec.rb'
- 'spec/features/crops/crop_detail_page_spec.rb' - 'spec/features/crops/crop_detail_page_spec.rb'
- 'spec/features/crops/crop_wranglers_spec.rb' - 'spec/features/crops/crop_wranglers_spec.rb'
- 'spec/features/gardens/gardens_spec.rb' - 'spec/features/gardens/gardens_spec.rb'
- 'spec/features/members/blocking_spec.rb'
- 'spec/features/members/deletion_spec.rb' - 'spec/features/members/deletion_spec.rb'
- 'spec/features/members/following_spec.rb' - 'spec/features/members/following_spec.rb'
- 'spec/features/members/profile_spec.rb' - 'spec/features/members/profile_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb' - 'spec/features/plantings/planting_a_crop_spec.rb'
# Offense count: 14 # Offense count: 14
Capybara/SpecificMatcher: Capybara/RSpec/SpecificMatcher:
Exclude: Exclude:
- 'spec/features/footer_spec.rb' - 'spec/features/footer_spec.rb'
- 'spec/features/gardens/adding_gardens_spec.rb' - 'spec/features/gardens/adding_gardens_spec.rb'
@@ -28,7 +34,7 @@ Capybara/SpecificMatcher:
- 'spec/features/seeds/adding_seeds_spec.rb' - 'spec/features/seeds/adding_seeds_spec.rb'
# Offense count: 1 # Offense count: 1
Capybara/VisibilityMatcher: Capybara/RSpec/VisibilityMatcher:
Exclude: Exclude:
- 'spec/features/shared_examples/crop_suggest.rb' - 'spec/features/shared_examples/crop_suggest.rb'
@@ -63,7 +69,13 @@ FactoryBot/ExcessiveCreateList:
- 'spec/features/crops/show_spec.rb' - 'spec/features/crops/show_spec.rb'
- 'spec/features/percy/percy_spec.rb' - 'spec/features/percy/percy_spec.rb'
# Offense count: 312 # Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Layout/EmptyLines:
Exclude:
- 'config/environments/production.rb'
# Offense count: 311
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle. # Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table # SupportedHashRocketStyles: key, separator, table
@@ -81,7 +93,7 @@ Layout/HashAlignment:
- 'spec/requests/api/v1/activities_request_spec.rb' - 'spec/requests/api/v1/activities_request_spec.rb'
- 'spec/requests/api/v1/members_request_spec.rb' - 'spec/requests/api/v1/members_request_spec.rb'
# Offense count: 6 # Offense count: 8
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings. # Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https # URISchemes: http, https
@@ -89,10 +101,20 @@ Layout/LineLength:
Exclude: Exclude:
- 'Gemfile' - 'Gemfile'
- 'app/controllers/admin/versions_controller.rb' - 'app/controllers/admin/versions_controller.rb'
- 'app/controllers/crops_controller.rb'
- 'app/models/concerns/predict_planting.rb' - 'app/models/concerns/predict_planting.rb'
- 'app/models/crop.rb' - 'app/models/crop.rb'
- 'db/seeds.rb' - 'db/seeds.rb'
- 'spec/requests/api/v1/activities_request_spec.rb' - 'lib/tasks/members.rake'
# Offense count: 3
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented, indented_relative_to_receiver
Layout/MultilineMethodCallIndentation:
Exclude:
- 'app/models/activity.rb'
- 'app/models/concerns/predict_harvest.rb'
# Offense count: 1 # Offense count: 1
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
@@ -100,23 +122,15 @@ Lint/AmbiguousOperatorPrecedence:
Exclude: Exclude:
- 'app/controllers/activities_controller.rb' - 'app/controllers/activities_controller.rb'
# Offense count: 4 # Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: RequireParenthesesForMethodChains. # Configuration parameters: RequireParenthesesForMethodChains.
Lint/AmbiguousRange: Lint/AmbiguousRange:
Exclude: Exclude:
- 'app/models/concerns/search_activities.rb'
- 'app/models/concerns/search_harvests.rb' - 'app/models/concerns/search_harvests.rb'
- 'app/models/concerns/search_plantings.rb' - 'app/models/concerns/search_plantings.rb'
- 'db/seeds.rb' - 'db/seeds.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Exclude:
- 'app/helpers/crops_helper.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: AllowedMethods. # Configuration parameters: AllowedMethods.
# AllowedMethods: enums # AllowedMethods: enums
@@ -154,27 +168,12 @@ Lint/SuppressedException:
Exclude: Exclude:
- 'lib/tasks/testing.rake' - 'lib/tasks/testing.rake'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: strict, consistent
Lint/SymbolConversion:
Exclude:
- 'app/helpers/crops_helper.rb'
# Offense count: 7
# This cop supports safe autocorrection (--autocorrect).
Lint/UselessAssignment:
Exclude:
- 'config.rb'
- 'config/compass.rb'
# Offense count: 1 # Offense count: 1
Lint/UselessConstantScoping: Lint/UselessConstantScoping:
Exclude: Exclude:
- 'app/controllers/members_controller.rb' - 'app/controllers/members_controller.rb'
# Offense count: 61 # Offense count: 65
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes. # Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize: Metrics/AbcSize:
Max: 295 Max: 295
@@ -196,12 +195,12 @@ Metrics/CollectionLiteralLength:
Exclude: Exclude:
- 'lib/tasks/import.rake' - 'lib/tasks/import.rake'
# Offense count: 10 # Offense count: 11
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity: Metrics/CyclomaticComplexity:
Max: 32 Max: 32
# Offense count: 82 # Offense count: 83
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns. # Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength: Metrics/MethodLength:
Max: 296 Max: 296
@@ -211,7 +210,7 @@ Metrics/MethodLength:
Metrics/ModuleLength: Metrics/ModuleLength:
Max: 144 Max: 144
# Offense count: 8 # Offense count: 10
# Configuration parameters: AllowedMethods, AllowedPatterns. # Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity: Metrics/PerceivedComplexity:
Max: 32 Max: 32
@@ -225,6 +224,16 @@ Naming/PredicateMethod:
- 'app/models/concerns/finishable.rb' - 'app/models/concerns/finishable.rb'
- 'app/models/seed.rb' - 'app/models/seed.rb'
# Offense count: 1
# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
# NamePrefix: is_, has_, have_, does_
# ForbiddenPrefixes: is_, has_, have_, does_
# AllowedMethods: is_a?
# MethodDefinitionMacros: define_method, define_singleton_method
Naming/PredicatePrefix:
Exclude:
- 'app/models/member.rb'
# Offense count: 3 # Offense count: 3
RSpec/AnyInstance: RSpec/AnyInstance:
Exclude: Exclude:
@@ -237,89 +246,54 @@ RSpec/BeEq:
Exclude: Exclude:
- 'spec/requests/api/v1/activities_request_spec.rb' - 'spec/requests/api/v1/activities_request_spec.rb'
# Offense count: 1 # Offense count: 2
RSpec/BeforeAfterAll: RSpec/BeforeAfterAll:
Exclude: Exclude:
- 'spec/tasks/import_spec.rb' - 'spec/tasks/import_spec.rb'
- 'spec/tasks/members_spec.rb'
# Offense count: 1 # Offense count: 311
# This cop supports safe autocorrection (--autocorrect).
RSpec/ContextMethod:
Exclude:
- 'spec/requests/api/v1/activities_request_spec.rb'
# Offense count: 299
# Configuration parameters: Prefixes, AllowedPatterns. # Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without # Prefixes: when, with, without
RSpec/ContextWording: RSpec/ContextWording:
Enabled: false Enabled: false
# Offense count: 1 # Offense count: 2
# Configuration parameters: IgnoredMetadata. # Configuration parameters: IgnoredMetadata.
RSpec/DescribeClass: RSpec/DescribeClass:
Exclude: Exclude:
- 'spec/tasks/import_spec.rb' - 'spec/models/harvest_prediction_spec.rb'
- 'spec/tasks/members_spec.rb'
# Offense count: 36 # Offense count: 37
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants. # Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
# SupportedStyles: described_class, explicit # SupportedStyles: described_class, explicit
RSpec/DescribedClass: RSpec/DescribedClass:
Exclude: Exclude:
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
- 'spec/models/like_spec.rb' - 'spec/models/like_spec.rb'
- 'spec/models/member_spec.rb' - 'spec/models/member_spec.rb'
- 'spec/services/timeline_service_spec.rb' - 'spec/services/timeline_service_spec.rb'
# Offense count: 13
# This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/EmptyExampleGroup:
Exclude:
- 'spec/controllers/authentications_controller_spec.rb'
- 'spec/controllers/forums_controller_spec.rb'
- 'spec/controllers/home_controller_spec.rb'
- 'spec/controllers/likes_controller_spec.rb'
- 'spec/controllers/plant_parts_controller_spec.rb'
- 'spec/controllers/seeds_controller_spec.rb'
- 'spec/features/crops/crop_detail_page_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/requests/authentications_spec.rb'
- 'spec/views/home/index_spec.rb'
- 'spec/views/photos/edit.html.haml_spec.rb'
- 'spec/views/posts/_single.html.haml_spec.rb'
# Offense count: 1 # Offense count: 1
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowConsecutiveOneLiners. # Configuration parameters: AllowConsecutiveOneLiners.
RSpec/EmptyLineAfterExample: RSpec/EmptyLineAfterExample:
Exclude: Exclude:
- 'spec/models/ability_spec.rb' - 'spec/controllers/crops_controller_spec.rb'
# Offense count: 146 # Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
RSpec/EmptyLineAfterFinalLet:
Exclude:
- 'spec/controllers/crops_controller_spec.rb'
# Offense count: 161
# Configuration parameters: CountAsOne. # Configuration parameters: CountAsOne.
RSpec/ExampleLength: RSpec/ExampleLength:
Max: 27 Max: 27
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: method_call, block
RSpec/ExpectChange:
Exclude:
- 'spec/models/crop_spec.rb'
# Offense count: 32
RSpec/ExpectInHook:
Exclude:
- 'spec/controllers/garden_types_controller_spec.rb'
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/features/admin/forums_spec.rb'
- 'spec/features/admin/plant_parts_spec.rb'
- 'spec/features/admin/roles_spec.rb'
- 'spec/features/crops/crop_photos_spec.rb'
- 'spec/features/members/list_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/features/shared_examples/append_date.rb'
# Offense count: 1 # Offense count: 1
# This cop supports safe autocorrection (--autocorrect). # This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
@@ -354,28 +328,18 @@ RSpec/IncludeExamples:
- 'spec/views/photos/show.html.haml_spec.rb' - 'spec/views/photos/show.html.haml_spec.rb'
- 'spec/views/seeds/index.rss.haml_spec.rb' - 'spec/views/seeds/index.rss.haml_spec.rb'
# Offense count: 37 # Offense count: 2
# Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns. # Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns.
RSpec/IndexedLet: RSpec/IndexedLet:
Exclude: Exclude:
- 'spec/controllers/harvests_controller_spec.rb' - 'spec/models/activity_spec.rb'
- 'spec/controllers/plantings_controller_spec.rb'
- 'spec/features/crops/crop_photos_spec.rb'
- 'spec/features/members/list_spec.rb'
- 'spec/features/members/profile_spec.rb'
- 'spec/features/percy/percy_spec.rb'
- 'spec/features/planting_reminder_spec.rb'
- 'spec/features/timeline/index_spec.rb'
- 'spec/models/crop_spec.rb'
- 'spec/models/member_spec.rb'
- 'spec/views/forums/index.html.haml_spec.rb'
# Offense count: 719 # Offense count: 711
# Configuration parameters: AssignmentOnly. # Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable: RSpec/InstanceVariable:
Enabled: false Enabled: false
# Offense count: 41 # Offense count: 43
RSpec/LetSetup: RSpec/LetSetup:
Enabled: false Enabled: false
@@ -390,7 +354,7 @@ RSpec/MessageChain:
Exclude: Exclude:
- 'spec/models/member_spec.rb' - 'spec/models/member_spec.rb'
# Offense count: 23 # Offense count: 65
# Configuration parameters: . # Configuration parameters: .
# SupportedStyles: have_received, receive # SupportedStyles: have_received, receive
RSpec/MessageSpies: RSpec/MessageSpies:
@@ -401,11 +365,11 @@ RSpec/MultipleDescribes:
Exclude: Exclude:
- 'spec/features/crops/crop_wranglers_spec.rb' - 'spec/features/crops/crop_wranglers_spec.rb'
# Offense count: 189 # Offense count: 235
RSpec/MultipleExpectations: RSpec/MultipleExpectations:
Max: 19 Max: 19
# Offense count: 166 # Offense count: 171
# Configuration parameters: AllowSubject. # Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers: RSpec/MultipleMemoizedHelpers:
Max: 16 Max: 16
@@ -416,24 +380,35 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NamedSubject: RSpec/NamedSubject:
Enabled: false Enabled: false
# Offense count: 111 # Offense count: 112
# Configuration parameters: AllowedGroups. # Configuration parameters: AllowedGroups.
RSpec/NestedGroups: RSpec/NestedGroups:
Max: 6 Max: 6
# Offense count: 407 # Offense count: 366
# Configuration parameters: AllowedPatterns. # Configuration parameters: AllowedPatterns.
# AllowedPatterns: ^expect_, ^assert_ # AllowedPatterns: ^expect_, ^assert_
RSpec/NoExpectationExample: RSpec/NoExpectationExample:
Enabled: false Enabled: false
# Offense count: 4 # Offense count: 9
RSpec/PendingWithoutReason: RSpec/PendingWithoutReason:
Exclude: Exclude:
- 'spec/features/members/blocking_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/features/seeds/misc_seeds_spec.rb' - 'spec/features/seeds/misc_seeds_spec.rb'
- 'spec/features/unsubscribing_spec.rb' - 'spec/features/unsubscribing_spec.rb'
- 'spec/models/ability_spec.rb'
- 'spec/requests/api/v1/gardens_request_spec.rb' - 'spec/requests/api/v1/gardens_request_spec.rb'
# Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers.
# SupportedStyles: inflected, explicit
RSpec/PredicateMatcher:
Exclude:
- 'spec/tasks/members_spec.rb'
# Offense count: 2 # Offense count: 2
RSpec/RepeatedDescription: RSpec/RepeatedDescription:
Exclude: Exclude:
@@ -458,18 +433,18 @@ RSpec/ScatteredSetup:
- 'spec/features/percy/percy_spec.rb' - 'spec/features/percy/percy_spec.rb'
- 'spec/features/plantings/prediction_spec.rb' - 'spec/features/plantings/prediction_spec.rb'
# Offense count: 1 # Offense count: 2
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector. # Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
# SupportedInflectors: default, active_support # SupportedInflectors: default, active_support
RSpec/SpecFilePathFormat: RSpec/SpecFilePathFormat:
Exclude: Exclude:
- 'spec/controllers/member_controller_spec.rb' - 'spec/controllers/member_controller_spec.rb'
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
# Offense count: 3 # Offense count: 2
RSpec/StubbedMock: RSpec/StubbedMock:
Exclude: Exclude:
- 'spec/controllers/garden_types_controller_spec.rb' - 'spec/controllers/photos_controller_spec.rb'
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/models/member_spec.rb' - 'spec/models/member_spec.rb'
# Offense count: 1 # Offense count: 1
@@ -486,22 +461,12 @@ RSpec/VerifiedDoubles:
- 'spec/controllers/gardens_controller_spec.rb' - 'spec/controllers/gardens_controller_spec.rb'
- 'spec/views/devise/shared/_links_spec.rb' - 'spec/views/devise/shared/_links_spec.rb'
# Offense count: 7 # Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: ResponseMethods.
# ResponseMethods: response, last_response
RSpecRails/HaveHttpStatus:
Exclude:
- 'spec/controllers/api/v1/plantings_controller_spec.rb'
- 'spec/controllers/harvests_controller_spec.rb'
- 'spec/controllers/likes_controller_spec.rb'
- 'spec/requests/harvests_spec.rb'
# Offense count: 17
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Inferences. # Configuration parameters: Inferences.
RSpecRails/InferredSpecType: RSpecRails/InferredSpecType:
Enabled: false Exclude:
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
# Offense count: 30 # Offense count: 30
# Configuration parameters: Database. # Configuration parameters: Database.
@@ -559,14 +524,24 @@ Rails/HasManyOrHasOneDependent:
- 'app/models/crop.rb' - 'app/models/crop.rb'
- 'app/models/member.rb' - 'app/models/member.rb'
# Offense count: 7
Rails/HelperInstanceVariable:
Exclude:
- 'app/helpers/crops_helper.rb'
- 'app/helpers/plantings_helper.rb'
# Offense count: 1 # Offense count: 1
Rails/I18nLocaleAssignment: Rails/I18nLocaleAssignment:
Exclude: Exclude:
- 'spec/features/locale_spec.rb' - 'spec/features/locale_spec.rb'
# Offense count: 40 # Offense count: 5
Rails/I18nLocaleTexts: Rails/I18nLocaleTexts:
Enabled: false Exclude:
- 'app/controllers/blocks_controller.rb'
- 'app/controllers/comments_controller.rb'
- 'app/controllers/messages_controller.rb'
- 'config/initializers/comfortable_mexican_sofa.rb'
# Offense count: 1 # Offense count: 1
# Configuration parameters: IgnoreScopes. # Configuration parameters: IgnoreScopes.
@@ -581,6 +556,12 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/data_controller.rb' - 'app/controllers/data_controller.rb'
- 'app/controllers/registrations_controller.rb' - 'app/controllers/registrations_controller.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/OrderArguments:
Exclude:
- 'app/models/activity.rb'
# Offense count: 2 # Offense count: 2
Rails/OutputSafety: Rails/OutputSafety:
Exclude: Exclude:
@@ -609,7 +590,7 @@ Rails/RakeEnvironment:
- 'lib/tasks/i18n.rake' - 'lib/tasks/i18n.rake'
- 'lib/tasks/testing.rake' - 'lib/tasks/testing.rake'
# Offense count: 9 # Offense count: 8
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedReceivers. # Configuration parameters: AllowedReceivers.
# AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone # AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone
@@ -622,7 +603,6 @@ Rails/RedundantActiveRecordAllMethod:
- 'app/controllers/scientific_names_controller.rb' - 'app/controllers/scientific_names_controller.rb'
- 'spec/features/members/deletion_spec.rb' - 'spec/features/members/deletion_spec.rb'
- 'spec/features/percy/percy_spec.rb' - 'spec/features/percy/percy_spec.rb'
- 'spec/models/harvest_spec.rb'
# Offense count: 5 # Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
@@ -664,15 +644,6 @@ Rails/RootPathnameMethods:
- 'lib/tasks/import.rake' - 'lib/tasks/import.rake'
- 'spec/rails_helper.rb' - 'spec/rails_helper.rb'
# Offense count: 4
# Configuration parameters: ForbiddenMethods, AllowedMethods.
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
Rails/SkipsModelValidations:
Exclude:
- 'db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb'
- 'db/migrate/20240810160538_set_default_language_for_existing_alternate_names.rb'
- 'db/migrate/20251128200506_add_description_to_crops.rb'
# Offense count: 21 # Offense count: 21
Rails/ThreeStateBooleanColumn: Rails/ThreeStateBooleanColumn:
Enabled: false Enabled: false
@@ -704,12 +675,6 @@ Rails/WhereEquals:
- 'app/models/harvest.rb' - 'app/models/harvest.rb'
- 'app/models/planting.rb' - 'app/models/planting.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Rails/WhereMissing:
Exclude:
- 'app/controllers/crops_controller.rb'
# Offense count: 3 # Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Rails/WhereRange: Rails/WhereRange:
@@ -723,7 +688,7 @@ Rake/MethodDefinitionInTask:
Exclude: Exclude:
- 'lib/tasks/growstuff.rake' - 'lib/tasks/growstuff.rake'
# Offense count: 4 # Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules. # Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
# SupportedStyles: nested, compact # SupportedStyles: nested, compact
@@ -732,10 +697,17 @@ Rake/MethodDefinitionInTask:
Style/ClassAndModuleChildren: Style/ClassAndModuleChildren:
Exclude: Exclude:
- 'app/controllers/admin/crops_controller.rb' - 'app/controllers/admin/crops_controller.rb'
- 'config/initializers/rack_attack.rb'
- 'lib/actions/oauth_signup_action.rb' - 'lib/actions/oauth_signup_action.rb'
- 'lib/haml/filters/escaped_markdown.rb' - 'lib/haml/filters/escaped_markdown.rb'
- 'lib/haml/filters/growstuff_markdown.rb' - 'lib/haml/filters/growstuff_markdown.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/CollectionQuerying:
Exclude:
- 'app/models/member.rb'
# Offense count: 6 # Offense count: 6
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
Style/CommentedKeyword: Style/CommentedKeyword:
@@ -766,12 +738,14 @@ Style/FloatDivision:
Exclude: Exclude:
- 'app/models/concerns/predict_planting.rb' - 'app/models/concerns/predict_planting.rb'
# Offense count: 22 # Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle. # Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never # SupportedStyles: always, always_true, never
Style/FrozenStringLiteralComment: Style/FrozenStringLiteralComment:
Enabled: false Exclude:
- 'db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb'
- 'spec/lib/haml/filters/growstuff_markdown_spec.rb'
# Offense count: 2 # Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
@@ -786,11 +760,14 @@ Style/IdenticalConditionalBranches:
Exclude: Exclude:
- 'lib/actions/oauth_signup_action.rb' - 'lib/actions/oauth_signup_action.rb'
# Offense count: 1 # Offense count: 4
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports safe autocorrection (--autocorrect).
Style/MapIntoArray: Style/IfUnlessModifier:
Exclude: Exclude:
- 'app/helpers/crops_helper.rb' - 'app/helpers/crops_helper.rb'
- 'app/models/concerns/predict_planting.rb'
- 'config/initializers/rack_attack.rb'
- 'lib/tasks/growstuff.rake'
# Offense count: 1 # Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
@@ -837,13 +814,6 @@ Style/OptionalBooleanParameter:
- 'app/helpers/application_helper.rb' - 'app/helpers/application_helper.rb'
- 'app/models/concerns/member_newsletter.rb' - 'app/models/concerns/member_newsletter.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'db/migrate/20251130035700_create_versions.rb'
# Offense count: 1 # Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Methods. # Configuration parameters: Methods.
@@ -851,6 +821,13 @@ Style/RedundantArgument:
Exclude: Exclude:
- 'app/helpers/application_helper.rb' - 'app/helpers/application_helper.rb'
# Offense count: 3
# This cop supports safe autocorrection (--autocorrect).
Style/RedundantBegin:
Exclude:
- 'app/models/concerns/predict_harvest.rb'
- 'app/models/harvest.rb'
# Offense count: 4 # Offense count: 4
# This cop supports unsafe autocorrection (--autocorrect-all). # This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeForConstants. # Configuration parameters: SafeForConstants.

View File

@@ -1 +1 @@
3.4.8 3.4.9

View File

@@ -248,6 +248,3 @@ linters:
ZeroUnit: ZeroUnit:
enabled: true enabled: true
Compass::*:
enabled: false

52
Dockerfile Normal file
View File

@@ -0,0 +1,52 @@
FROM ruby:3.4.9-trixie
# Install system dependencies
RUN apt-get update -qq && \
apt-get install -y --no-install-recommends \
build-essential \
libpq-dev \
git \
curl \
gnupg2 \
shared-mime-info \
libvips \
&& curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \
&& apt-get install -y --no-install-recommends nodejs \
&& npm install -g yarn \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Set production environment
ENV RAILS_ENV=production \
BUNDLE_WITHOUT="development test" \
RAILS_SERVE_STATIC_FILES=true \
RAILS_LOG_TO_STDOUT=true
WORKDIR /app
# Install gems
COPY Gemfile Gemfile.lock ./
RUN bundle config set --local deployment 'true' && \
bundle config set --local without 'development test' && \
bundle install --jobs 4 --retry 3
# Install JavaScript dependencies
COPY package.json yarn.lock ./
RUN yarn install --check-files
# Copy the application code
COPY . .
# Precompile assets
# Secret key base is needed for asset compilation but doesn't need to be the real one
RUN RAILS_ENV=production SECRET_KEY_BASE_DUMMY=1 bundle exec rake assets:precompile
# Add a script to be executed every time the container starts.
COPY entrypoint.sh /usr/bin/
RUN chmod +x /usr/bin/entrypoint.sh
ENTRYPOINT ["entrypoint.sh"]
EXPOSE 3000
# Start the main process.
CMD ["bundle", "exec", "puma", "-C", "config/puma.rb"]

View File

@@ -116,6 +116,8 @@ gem 'xmlrpc' # fixes rake error - can be removed if not needed later
gem 'puma' gem 'puma'
gem 'rack-attack'
gem 'loofah', '>= 2.19.1' gem 'loofah', '>= 2.19.1'
gem 'rack-protection', '>= 2.0.1' gem 'rack-protection', '>= 2.0.1'

View File

@@ -122,8 +122,8 @@ GEM
autoprefixer-rails (10.4.16.0) autoprefixer-rails (10.4.16.0)
execjs (~> 2) execjs (~> 2)
aws-eventstream (1.4.0) aws-eventstream (1.4.0)
aws-partitions (1.1240.0) aws-partitions (1.1252.0)
aws-sdk-core (3.245.0) aws-sdk-core (3.248.0)
aws-eventstream (~> 1, >= 1.3.0) aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.992.0) aws-partitions (~> 1, >= 1.992.0)
aws-sigv4 (~> 1.9) aws-sigv4 (~> 1.9)
@@ -131,24 +131,24 @@ GEM
bigdecimal bigdecimal
jmespath (~> 1, >= 1.6.1) jmespath (~> 1, >= 1.6.1)
logger logger
aws-sdk-kms (1.123.0) aws-sdk-kms (1.128.0)
aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-core (~> 3, >= 3.248.0)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sdk-s3 (1.220.0) aws-sdk-s3 (1.224.0)
aws-sdk-core (~> 3, >= 3.244.0) aws-sdk-core (~> 3, >= 3.248.0)
aws-sdk-kms (~> 1) aws-sdk-kms (~> 1)
aws-sigv4 (~> 1.5) aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1) aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2) aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.11.2) axe-core-api (4.11.3)
dumb_delegator dumb_delegator
ostruct ostruct
virtus virtus
axe-core-capybara (4.11.2) axe-core-capybara (4.11.3)
axe-core-api (= 4.11.2) axe-core-api (= 4.11.3)
dumb_delegator dumb_delegator
axe-core-rspec (4.11.2) axe-core-rspec (4.11.3)
axe-core-api (= 4.11.2) axe-core-api (= 4.11.3)
dumb_delegator dumb_delegator
ostruct ostruct
virtus virtus
@@ -236,7 +236,7 @@ GEM
csv_shaper (1.4.0) csv_shaper (1.4.0)
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
csv csv
dalli (5.0.2) dalli (5.0.4)
logger logger
database_cleaner (2.1.0) database_cleaner (2.1.0)
database_cleaner-active_record (>= 2, < 3) database_cleaner-active_record (>= 2, < 3)
@@ -247,7 +247,7 @@ GEM
date (3.5.1) date (3.5.1)
descendants_tracker (0.0.4) descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1) thread_safe (~> 0.3, >= 0.3.1)
devise (5.0.3) devise (5.0.4)
bcrypt (~> 3.0) bcrypt (~> 3.0)
orm_adapter (~> 0.1) orm_adapter (~> 0.1)
railties (>= 7.0) railties (>= 7.0)
@@ -276,7 +276,7 @@ GEM
elasticsearch-transport (7.0.0) elasticsearch-transport (7.0.0)
faraday faraday
multi_json multi_json
erb (6.0.2) erb (6.0.4)
erubi (1.13.1) erubi (1.13.1)
execjs (2.10.0) execjs (2.10.0)
factory_bot (6.5.5) factory_bot (6.5.5)
@@ -286,7 +286,7 @@ GEM
railties (>= 6.1.0) railties (>= 6.1.0)
faker (3.8.0) faker (3.8.0)
i18n (>= 1.8.11, < 2) i18n (>= 1.8.11, < 2)
faraday (2.14.1) faraday (2.14.2)
faraday-net_http (>= 2.0, < 3.5) faraday-net_http (>= 2.0, < 3.5)
json json
logger logger
@@ -297,7 +297,7 @@ GEM
flickraw (0.9.10) flickraw (0.9.10)
font-awesome-sass (5.15.1) font-awesome-sass (5.15.1)
sassc (>= 1.11) sassc (>= 1.11)
friendly_id (5.6.0) friendly_id (5.7.0)
activerecord (>= 4.0.0) activerecord (>= 4.0.0)
gbifrb (0.2.0) gbifrb (0.2.0)
geocoder (1.8.6) geocoder (1.8.6)
@@ -355,7 +355,7 @@ GEM
terminal-table (>= 1.5.1) terminal-table (>= 1.5.1)
i18n_data (1.1.0) i18n_data (1.1.0)
simple_po_parser (~> 1.1) simple_po_parser (~> 1.1)
icalendar (2.12.2) icalendar (2.12.3)
base64 base64
ice_cube (~> 0.16) ice_cube (~> 0.16)
logger logger
@@ -366,7 +366,7 @@ GEM
mini_magick (>= 4.9.5, < 5) mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3) ruby-vips (>= 2.0.17, < 3)
io-console (0.8.2) io-console (0.8.2)
irb (1.17.0) irb (1.18.0)
pp (>= 0.6.0) pp (>= 0.6.0)
prism (>= 1.3.0) prism (>= 1.3.0)
rdoc (>= 4.0.0) rdoc (>= 4.0.0)
@@ -376,7 +376,7 @@ GEM
rails-dom-testing (>= 1, < 3) rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0) railties (>= 4.2.0)
thor (>= 0.14, < 2.0) thor (>= 0.14, < 2.0)
json (2.19.3) json (2.19.5)
json-schema (6.2.0) json-schema (6.2.0)
addressable (~> 2.8) addressable (~> 2.8)
bigdecimal (>= 3.1, < 5) bigdecimal (>= 3.1, < 5)
@@ -453,13 +453,13 @@ GEM
net-protocol net-protocol
netrc (0.11.0) netrc (0.11.0)
nio4r (2.7.5) nio4r (2.7.5)
nokogiri (1.19.2) nokogiri (1.19.3)
mini_portile2 (~> 2.8.2) mini_portile2 (~> 2.8.2)
racc (~> 1.4) racc (~> 1.4)
nokogiri (1.19.2-x86_64-linux-gnu) nokogiri (1.19.3-x86_64-linux-gnu)
racc (~> 1.4) racc (~> 1.4)
oauth (0.5.6) oauth (0.5.6)
oj (3.17.0) oj (3.17.1)
bigdecimal (>= 3.0) bigdecimal (>= 3.0)
ostruct (>= 0.2) ostruct (>= 0.2)
omniauth (1.9.2) omniauth (1.9.2)
@@ -477,7 +477,7 @@ GEM
paper_trail (17.0.0) paper_trail (17.0.0)
activerecord (>= 7.1) activerecord (>= 7.1)
request_store (~> 1.4) request_store (~> 1.4)
parallel (2.0.1) parallel (2.1.0)
parser (3.3.11.1) parser (3.3.11.1)
ast (~> 2.4.1) ast (~> 2.4.1)
racc racc
@@ -498,11 +498,13 @@ GEM
date date
stringio stringio
public_suffix (7.0.5) public_suffix (7.0.5)
puma (8.0.0) puma (8.0.1)
nio4r (~> 2.0) nio4r (~> 2.0)
query_diet (0.7.3) query_diet (0.7.3)
racc (1.8.1) racc (1.8.1)
rack (2.2.23) rack (2.2.23)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2) rack-cors (2.0.2)
rack (>= 2.0.0) rack (>= 2.0.0)
rack-protection (3.2.0) rack-protection (3.2.0)
@@ -628,7 +630,7 @@ GEM
rswag-ui (2.17.0) rswag-ui (2.17.0)
actionpack (>= 5.2, < 8.2) actionpack (>= 5.2, < 8.2)
railties (>= 5.2, < 8.2) railties (>= 5.2, < 8.2)
rubocop (1.86.1) rubocop (1.86.2)
json (~> 2.3) json (~> 2.3)
language_server-protocol (~> 3.17.0.2) language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0) lint_roller (~> 1.1.0)
@@ -642,13 +644,13 @@ GEM
rubocop-ast (1.49.1) rubocop-ast (1.49.1)
parser (>= 3.3.7.2) parser (>= 3.3.7.2)
prism (~> 1.7) prism (~> 1.7)
rubocop-capybara (2.22.1) rubocop-capybara (2.23.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.81)
rubocop-factory_bot (2.28.0) rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1) rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.34.3) rubocop-rails (2.35.2)
activesupport (>= 4.2.0) activesupport (>= 4.2.0)
lint_roller (~> 1.1) lint_roller (~> 1.1)
rack (>= 1.1) rack (>= 1.1)
@@ -668,7 +670,7 @@ GEM
ruby-units (4.1.0) ruby-units (4.1.0)
ruby-vips (2.2.1) ruby-vips (2.2.1)
ffi (~> 1.12) ffi (~> 1.12)
rubyzip (3.2.2) rubyzip (3.3.0)
sass (3.7.4) sass (3.7.4)
sass-listen (~> 4.0.0) sass-listen (~> 4.0.0)
sass-listen (4.0.0) sass-listen (4.0.0)
@@ -688,7 +690,7 @@ GEM
activemodel (>= 6.1) activemodel (>= 6.1)
hashie hashie
securerandom (0.4.1) securerandom (0.4.1)
selenium-webdriver (4.43.0) selenium-webdriver (4.44.0)
base64 (~> 0.2) base64 (~> 0.2)
logger (~> 1.4) logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5) rexml (~> 3.2, >= 3.2.5)
@@ -723,7 +725,7 @@ GEM
thread_safe (0.3.6) thread_safe (0.3.6)
tilt (2.7.0) tilt (2.7.0)
timecop (0.9.11) timecop (0.9.11)
timeout (0.5.0) timeout (0.6.1)
tsort (0.2.0) tsort (0.2.0)
tzinfo (2.0.6) tzinfo (2.0.6)
concurrent-ruby (~> 1.0) concurrent-ruby (~> 1.0)
@@ -841,6 +843,7 @@ DEPENDENCIES
pry pry
puma puma
query_diet query_diet
rack-attack
rack-cors rack-cors
rack-protection (>= 2.0.1) rack-protection (>= 2.0.1)
rails (~> 7.2.0) rails (~> 7.2.0)
@@ -883,7 +886,7 @@ DEPENDENCIES
xmlrpc xmlrpc
RUBY VERSION RUBY VERSION
ruby 3.4.8p72 ruby 3.4.9
BUNDLED WITH BUNDLED WITH
2.4.22 2.4.22

View File

@@ -4,21 +4,16 @@ class ActivitiesController < DataController
def index def index
@show_all = params[:all] == '1' @show_all = params[:all] == '1'
where = {} @activities = Activity.includes(:owner).order(created_at: :desc)
where['active'] = true unless @show_all @activities = @activities.active unless @show_all
if params[:member_slug] if params[:member_slug].present?
@owner = Member.find_by(slug: params[:member_slug]) @owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id unless @owner.nil? @activities = @activities.where(owner_id: @owner.id)
end end
@activities = Activity.search( @activities = @activities.paginate(page: params[:page], per_page: 30)
where:,
page: params[:page],
limit: 30,
boost_by: [:created_at],
load: false
)
@filename = "Growstuff-#{specifics}Activities-#{Time.zone.now.to_fs(:number)}.csv" @filename = "Growstuff-#{specifics}Activities-#{Time.zone.now.to_fs(:number)}.csv"
respond_with(@activities) respond_with(@activities)
end end

View File

@@ -15,7 +15,7 @@ module Admin
def create def create
@crop_companion = @crop.crop_companions.new(crop_companion_params) @crop_companion = @crop.crop_companions.new(crop_companion_params)
if @crop_companion.save if @crop_companion.save
redirect_to admin_crop_crop_companions_path(@crop), notice: 'Companion was successfully created.' redirect_to admin_crop_crop_companions_path(@crop), notice: t('crop_companions.created')
else else
render :new render :new
end end
@@ -24,7 +24,7 @@ module Admin
def destroy def destroy
@crop_companion = @crop.crop_companions.find(params[:id]) @crop_companion = @crop.crop_companions.find(params[:id])
@crop_companion.destroy @crop_companion.destroy
redirect_to admin_crop_crop_companions_path(@crop), notice: 'Companion was successfully destroyed.' redirect_to admin_crop_crop_companions_path(@crop), notice: t('crop_companions.deleted')
end end
private private

View File

@@ -8,9 +8,9 @@ module Admin
responders :flash responders :flash
def index def index
@members = Member.all @members = Member.order(:login_name)
@members = @members.where("login_name ILIKE ?", "%#{search_term}%") unless search_term.nil? @members = @members.where("login_name ILIKE ?", "%#{search_term}%") if search_term.present?
@members = @members.order(:login_name).paginate(page: params[:page]) @members = @members.paginate(page: params[:page])
end end
def edit def edit

View File

@@ -9,9 +9,9 @@ module Admin
@version = PaperTrail::Version.find(params[:id]) @version = PaperTrail::Version.find(params[:id])
@object = @version.reify @object = @version.reify
if @object.save if @object.save
redirect_to admin_crops_path, notice: "Reverted to version from #{@version.created_at.strftime('%B %d, %Y')}" redirect_to admin_crops_path, notice: t('messages.revert_success', date: @version.created_at.strftime('%B %d, %Y'))
else else
redirect_to admin_crops_path, alert: "Could not revert to version from #{@version.created_at.strftime('%B %d, %Y')}. Errors: #{@object.errors.full_messages.to_sentence}" redirect_to admin_crops_path, alert: t('messages.revert_error', date: @version.created_at.strftime('%B %d, %Y'), errors: @object.errors.full_messages.to_sentence)
end end
end end

View File

@@ -30,7 +30,7 @@ class AlternateNamesController < ApplicationController
@alternate_name = AlternateName.new(alternate_name_params) @alternate_name = AlternateName.new(alternate_name_params)
if @alternate_name.save if @alternate_name.save
redirect_to @alternate_name.crop, notice: 'Alternate name was successfully created.' redirect_to @alternate_name.crop, notice: t('alternate_names.created')
else else
render action: "new" render action: "new"
end end
@@ -40,7 +40,7 @@ class AlternateNamesController < ApplicationController
# PUT /alternate_names/1.json # PUT /alternate_names/1.json
def update def update
if @alternate_name.update(alternate_name_params) if @alternate_name.update(alternate_name_params)
redirect_to @alternate_name.crop, notice: 'Alternate name was successfully updated.' redirect_to @alternate_name.crop, notice: t('alternate_names.updated')
else else
render action: "edit" render action: "edit"
end end
@@ -51,7 +51,7 @@ class AlternateNamesController < ApplicationController
def destroy def destroy
@crop = @alternate_name.crop @crop = @alternate_name.crop
@alternate_name.destroy @alternate_name.destroy
redirect_to @crop, notice: 'Alternate name was successfully deleted.' redirect_to @crop, notice: t('alternate_names.deleted')
end end
private private

View File

@@ -3,6 +3,36 @@
module Api module Api
module V1 module V1
class CropsController < BaseController class CropsController < BaseController
def search
term = params[:term]
page = params.dig(:page, :number) || 1
per_page = params.dig(:page, :size) || Crop.per_page
search_results = CropSearchService.search(
term,
page: page,
per_page: per_page,
load: true
)
resources = search_results.map do |crop|
Api::V1::CropResource.new(crop, context)
end
serializer = JSONAPI::ResourceSerializer.new(Api::V1::CropResource)
data = resources.map do |resource|
serializer.object_hash(resource, {})
end
render json: {
data: data,
meta: {
record_count: search_results.total_count,
page_count: search_results.total_pages
}
}
end
end end
end end
end end

View File

@@ -68,7 +68,7 @@ class ApplicationController < ActionController::Base
# profile stuff # profile stuff
:bio, :location, :latitude, :longitude, :bio, :location, :latitude, :longitude,
# email settings # email settings
:show_email, :newsletter, :send_notification_email, :send_planting_reminder) :show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder)
end end
devise_parameter_sanitizer.permit(:account_update) do |member| devise_parameter_sanitizer.permit(:account_update) do |member|
@@ -80,7 +80,7 @@ class ApplicationController < ActionController::Base
:bio, :location, :latitude, :longitude, :bio, :location, :latitude, :longitude,
:website_url, :instagram_handle, :facebook_handle, :bluesky_handle, :other_url, :website_url, :instagram_handle, :facebook_handle, :bluesky_handle, :other_url,
# email settings # email settings
:show_email, :newsletter, :send_notification_email, :send_planting_reminder, :show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder,
# update password # update password
:current_password) :current_password)
end end

View File

@@ -24,9 +24,9 @@ class AuthenticationsController < ApplicationController
name: name:
) )
flash[:notice] = "Authentication successful." flash[:notice] = t('messages.auth_success')
else else
flash[:notice] = "Authentication failed." flash[:notice] = t('messages.auth_failed')
end end
redirect_to request.env['omniauth.origin'] || edit_member_registration_path redirect_to request.env['omniauth.origin'] || edit_member_registration_path
end end

View File

@@ -0,0 +1,32 @@
# frozen_string_literal: true
class BlocksController < ApplicationController
load_and_authorize_resource
skip_load_resource only: :create
def create
@block = current_member.blocks.build(blocked: Member.find(params[:blocked]))
if @block.save
flash[:notice] = "Blocked #{@block.blocked.login_name}"
else
flash[:error] = "Already blocking or error while blocking."
end
redirect_back_or_to(root_path)
end
def destroy
@block = current_member.blocks.find(params[:id])
@unblocked = @block.blocked
@block.destroy
flash[:notice] = "Unblocked #{@unblocked.login_name}"
redirect_to @unblocked
end
private
def block_params
params.permit(:id, :blocked)
end
end

View File

@@ -3,6 +3,7 @@
module Charts module Charts
class CropsController < ApplicationController class CropsController < ApplicationController
respond_to :json respond_to :json
before_action :set_crop
def sunniness def sunniness
pie_chart_query 'sunniness' pie_chart_query 'sunniness'
@@ -13,20 +14,28 @@ module Charts
end end
def harvested_for def harvested_for
@crop = Crop.find_by!(slug: params[:crop_slug]) data = Rails.cache.fetch("#{@crop.cache_key_with_version}/harvested_for", expires_in: 1.day) do
render json: Harvest.joins(:plant_part) Harvest.joins(:plant_part)
.where(crop: @crop) .where(crop: @crop)
.group("plant_parts.name").count(:id) .group("plant_parts.name").count(:id)
end
render json: data
end end
private private
def pie_chart_query(field) def set_crop
@crop = Crop.find_by!(slug: params[:crop_slug]) @crop = Crop.find_by!(slug: params[:crop_slug])
render json: Planting.where(crop: @crop) end
.where.not(field.to_sym => nil)
.where.not(field.to_sym => '') def pie_chart_query(field)
.group(field.to_sym).count(:id) data = Rails.cache.fetch("#{@crop.cache_key_with_version}/#{field}", expires_in: 1.day) do
Planting.where(crop: @crop)
.where.not(field.to_sym => nil)
.where.not(field.to_sym => '')
.group(field.to_sym).count(:id)
end
render json: data
end end
end end
end end

View File

@@ -13,7 +13,7 @@ class CropsController < ApplicationController
@crops = Crop.search('*', boost_by: %i(plantings_count harvests_count), @crops = Crop.search('*', boost_by: %i(plantings_count harvests_count),
limit: 100, limit: 100,
page: params[:page], page: params[:page],
load: false) load: (request.format.csv? || request.format.rss? ? { include: %i(parent scientific_names seeds harvests creator plantings) } : false))
@num_requested_crops = requested_crops.size if current_member @num_requested_crops = requested_crops.size if current_member
@filename = filename @filename = filename
respond_with @crops respond_with @crops
@@ -160,7 +160,7 @@ class CropsController < ApplicationController
when 'youtube' when 'youtube'
Crop.approved.where(en_youtube_url: [nil, '']).order(plantings_count: :desc) Crop.approved.where(en_youtube_url: [nil, '']).order(plantings_count: :desc)
when 'alternate_names' when 'alternate_names'
Crop.approved.left_joins(:alternate_names).where(alternate_names: { id: nil }).order(plantings_count: :desc) Crop.approved.where.missing(:alternate_names).order(plantings_count: :desc)
when 'wikidata' when 'wikidata'
crops_with_wikidata = Crop.joins(:scientific_names).where.not(scientific_names: { wikidata_id: nil }).distinct crops_with_wikidata = Crop.joins(:scientific_names).where.not(scientific_names: { wikidata_id: nil }).distinct
Crop.approved.where.not(id: crops_with_wikidata).order(plantings_count: :desc) Crop.approved.where.not(id: crops_with_wikidata).order(plantings_count: :desc)

View File

@@ -13,9 +13,9 @@ class FollowsController < ApplicationController
@follow = current_member.follows.build(followed: Member.find(params[:followed])) @follow = current_member.follows.build(followed: Member.find(params[:followed]))
if @follow.save if @follow.save
flash[:notice] = "Followed #{@follow.followed.login_name}" flash[:notice] = t('messages.followed', name: @follow.followed.login_name)
else else
flash[:error] = "Already following or error while following." flash[:error] = t('messages.follow_error')
end end
redirect_back_or_to(root_path) redirect_back_or_to(root_path)
end end
@@ -25,7 +25,7 @@ class FollowsController < ApplicationController
@unfollowed = @follow.followed @unfollowed = @follow.followed
@follow.destroy @follow.destroy
flash[:notice] = "Unfollowed #{@unfollowed.login_name}" flash[:notice] = t('messages.unfollowed', name: @unfollowed.login_name)
redirect_to @unfollowed redirect_to @unfollowed
end end

View File

@@ -32,14 +32,14 @@ class ForumsController < ApplicationController
# POST /forums.json # POST /forums.json
def create def create
@forum = Forum.new(forum_params) @forum = Forum.new(forum_params)
flash[:notice] = 'Forum was successfully created.' if @forum.save flash[:notice] = t('forums.created') if @forum.save
respond_with(@forum) respond_with(@forum)
end end
# PUT /forums/1 # PUT /forums/1
# PUT /forums/1.json # PUT /forums/1.json
def update def update
flash[:notice] = 'Forum was successfully updated.' if @forum.update(forum_params) flash[:notice] = t('forums.updated') if @forum.update(forum_params)
respond_with(@forum) respond_with(@forum)
end end
@@ -47,7 +47,7 @@ class ForumsController < ApplicationController
# DELETE /forums/1.json # DELETE /forums/1.json
def destroy def destroy
@forum.destroy @forum.destroy
flash[:notice] = 'Forum was successfully deleted' flash[:notice] = t('forums.deleted')
redirect_to forums_url redirect_to forums_url
end end

View File

@@ -2,7 +2,7 @@
class GardensController < DataController class GardensController < DataController
def index def index
@owner = Member.find_by(slug: params[:member_slug]) @owner = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present?
@show_all = params[:all] == '1' @show_all = params[:all] == '1'
@show_jump_to = params[:member_slug].present? || false @show_jump_to = params[:member_slug].present? || false
@@ -57,12 +57,23 @@ class GardensController < DataController
redirect_to(member_gardens_path(@garden.owner)) redirect_to(member_gardens_path(@garden.owner))
end end
def fetch_wikidata
if @garden.populate_wikidata_info
@garden.save
flash[:notice] = "Wikidata information updated."
else
flash[:alert] = "Could not find Wikidata information for this location."
end
redirect_to @garden
end
private private
def garden_params def garden_params
params.require(:garden).permit( params.require(:garden).permit(
:name, :slug, :description, :active, :name, :slug, :description, :active,
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id :location, :latitude, :longitude, :area, :area_unit, :garden_type_id,
:location_wikidata_id, :lowest_temp_c, :highest_temp_c
) )
end end
end end

View File

@@ -5,8 +5,8 @@ class HarvestsController < DataController
def index def index
where = {} where = {}
if params[:member_slug] if params[:member_slug].present?
@owner = Member.find_by(slug: params[:member_slug]) @owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id where['owner_id'] = @owner.id
end end
@@ -23,7 +23,7 @@ class HarvestsController < DataController
@harvests = Harvest.search('*', where:, @harvests = Harvest.search('*', where:,
limit: 100, limit: 100,
page: params[:page], page: params[:page],
load: false, load: (request.format.csv? ? { include: %i(crop owner plant_part) } : false),
boost_by: [:created_at]) boost_by: [:created_at])
@filename = csv_filename @filename = csv_filename
@@ -38,9 +38,9 @@ class HarvestsController < DataController
end end
def new def new
@harvest = Harvest.new(harvested_at: Time.zone.today) @harvest = Harvest.new(new_harvest_params.merge(harvested_at: Time.zone.today))
@planting = Planting.find_by(slug: params[:planting_slug]) if params[:planting_slug] @planting = @harvest.planting
@crop = Crop.find_by(id: params[:crop_id]) @crop = @harvest.crop
respond_with(@harvest) respond_with(@harvest)
end end
@@ -52,7 +52,7 @@ class HarvestsController < DataController
def create def create
@harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id @harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id
@harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank? @harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank?
@harvest.save update_planting_rating if @harvest.save
if params[:return] == 'planting' if params[:return] == 'planting'
respond_with(@harvest, location: @harvest.planting) respond_with(@harvest, location: @harvest.planting)
else else
@@ -61,7 +61,7 @@ class HarvestsController < DataController
end end
def update def update
@harvest.update(harvest_params) update_planting_rating if @harvest.update(harvest_params)
respond_with(@harvest) respond_with(@harvest)
end end
@@ -76,7 +76,17 @@ class HarvestsController < DataController
params.require(:harvest) params.require(:harvest)
.permit(:planting_id, :crop_id, :harvested_at, :description, .permit(:planting_id, :crop_id, :harvested_at, :description,
:quantity, :unit, :weight_quantity, :weight_unit, :quantity, :unit, :weight_quantity, :weight_unit,
:plant_part_id, :slug, :si_weight) :plant_part_id, :slug, :si_weight, :overall_rating)
.merge(owner_id: current_member.id)
end
def new_harvest_params
return {} unless params[:harvest]
params.require(:harvest)
.permit(:planting_id, :crop_id, :harvested_at, :description,
:quantity, :unit, :weight_quantity, :weight_unit,
:plant_part_id, :slug, :si_weight, :overall_rating)
.merge(owner_id: current_member.id) .merge(owner_id: current_member.id)
end end
@@ -103,4 +113,10 @@ class HarvestsController < DataController
@harvest.planting.update_harvest_days! @harvest.planting.update_harvest_days!
@harvest.crop.update_harvest_medians @harvest.crop.update_harvest_medians
end end
def update_planting_rating
return if @harvest.planting.nil? || params[:harvest][:overall_rating].blank?
@harvest.planting.update(overall_rating: params[:harvest][:overall_rating])
end
end end

View File

@@ -14,7 +14,7 @@ class LikesController < ApplicationController
@like.likeable.reindex(refresh: true) @like.likeable.reindex(refresh: true)
success(@like, liked_by_member: true, status_code: :created) success(@like, liked_by_member: true, status_code: :created)
else else
failed(@like, message: 'Unable to like') failed(@like, message: t('messages.unable_to_like'))
end end
end end
@@ -29,7 +29,7 @@ class LikesController < ApplicationController
@like.likeable.reindex(refresh: true) @like.likeable.reindex(refresh: true)
success(@like, liked_by_member: false, status_code: :ok) success(@like, liked_by_member: false, status_code: :ok)
else else
failed(@like, message: 'Unable to unlike') failed(@like, message: t('messages.unable_to_unlike'))
end end
end end

View File

@@ -90,11 +90,12 @@ class MembersController < ApplicationController
EMAIL_TYPE_STRING = { EMAIL_TYPE_STRING = {
send_notification_email: "direct message notifications", send_notification_email: "direct message notifications",
send_planting_reminder: "planting reminders" send_planting_reminder: "planting reminders",
send_harvest_reminder: "harvest reminders"
}.freeze }.freeze
def member_params def member_params
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter) params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :send_harvest_reminder)
end end
def member_json_fields def member_json_fields

View File

@@ -27,10 +27,21 @@ class MessagesController < ApplicationController
def create def create
if params[:conversation_id].present? if params[:conversation_id].present?
@conversation = Mailboxer::Conversation.find(params[:conversation_id]) @conversation = Mailboxer::Conversation.find(params[:conversation_id])
# Check if any of the recipients have blocked the sender
if @conversation.recipients.any? { |recipient| recipient.already_blocking?(current_member) }
flash[:error] = "You cannot reply to this conversation because one of the recipients has blocked you."
redirect_to conversation_path(@conversation)
return
end
current_member.reply_to_conversation(@conversation, params[:body]) current_member.reply_to_conversation(@conversation, params[:body])
redirect_to conversation_path(@conversation) redirect_to conversation_path(@conversation)
else else
recipient = Member.find(params[:recipient_id]) recipient = Member.find(params[:recipient_id])
if recipient.already_blocking?(current_member)
flash[:error] = "You cannot send a message to a member who has blocked you."
redirect_back_or_to(root_path)
return
end
body = params[:body] body = params[:body]
subject = params[:subject] subject = params[:subject]
@conversation = current_member.send_message(recipient, body, subject) @conversation = current_member.send_message(recipient, body, subject)

View File

@@ -10,7 +10,7 @@ require './lib/actions/oauth_signup_action'
# #
class OmniauthCallbacksController < Devise::OmniauthCallbacksController class OmniauthCallbacksController < Devise::OmniauthCallbacksController
def failure def failure
flash[:alert] = "Authentication failed." flash[:alert] = t('messages.auth_failed')
redirect_to request.env['omniauth.origin'] || "/" redirect_to request.env['omniauth.origin'] || "/"
end end

View File

@@ -102,11 +102,12 @@ class PhotosController < ApplicationController
end end
@current_set = params[:set] @current_set = params[:set]
@current_tag = params[:tag]
page = params[:page] || 1 page = params[:page] || 1
@sets = current_member.flickr_sets @sets = current_member.flickr_sets
photos, total = current_member.flickr_photos(page, @current_set) photos, total = current_member.flickr_photos(page, @current_set, @current_tag)
@photos = WillPaginate::Collection.create(page, 30, total) do |pager| @photos = WillPaginate::Collection.create(page, 30, total) do |pager|
pager.replace photos pager.replace photos
@@ -118,6 +119,8 @@ class PhotosController < ApplicationController
{ crops: @crop.id } { crops: @crop.id }
elsif params[:planting_id] elsif params[:planting_id]
{ planting_id: @planting.id } { planting_id: @planting.id }
elsif params[:planting_slug]
{ plantings: @planting.id }
else else
{} {}
end end
@@ -126,5 +129,6 @@ class PhotosController < ApplicationController
def set_crop_and_planting def set_crop_and_planting
@crop = Crop.find params[:crop_slug] if params[:crop_slug] @crop = Crop.find params[:crop_slug] if params[:crop_slug]
@planting = Planting.find params[:planting_id] if params[:planting_id] @planting = Planting.find params[:planting_id] if params[:planting_id]
@planting ||= Planting.find params[:planting_slug] if params[:planting_slug]
end end
end end

View File

@@ -33,7 +33,7 @@ class PlacesController < ApplicationController
def search def search
if params[:new_place].empty? if params[:new_place].empty?
redirect_to places_path, alert: 'Please enter a valid location' redirect_to places_path, alert: t('messages.invalid_location')
else else
redirect_to place_path(params[:new_place]) redirect_to place_path(params[:new_place])
end end

View File

@@ -11,9 +11,9 @@ class PlantingsController < DataController
where = {} where = {}
where['active'] = true unless @show_all where['active'] = true unless @show_all
if params[:member_slug] if params[:member_slug].present?
@owner = Member.find_by(slug: params[:member_slug]) @owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id unless @owner.nil? where['owner_id'] = @owner.id
end end
if params[:crop_slug] if params[:crop_slug]
@@ -116,11 +116,11 @@ class PlantingsController < DataController
new_planting.finished_at = nil new_planting.finished_at = nil
if new_planting.save if new_planting.save
redirect_to edit_planting_path(new_planting), notice: 'Planting was successfully transplanted.' redirect_to edit_planting_path(new_planting), notice: t('messages.transplant_success')
else else
# if the save fails, we should probably roll back the finishing of the original planting # if the save fails, we should probably roll back the finishing of the original planting
@planting.update(finished: false, finished_at: nil) @planting.update(finished: false, finished_at: nil)
redirect_to @planting, alert: "There was an error transplanting the planting: #{new_planting.errors.full_messages.to_sentence}" redirect_to @planting, alert: t('messages.transplant_error', errors: new_planting.errors.full_messages.to_sentence)
end end
end end
@@ -160,7 +160,7 @@ class PlantingsController < DataController
end end
def matching_seeds def matching_seeds
Seed.where(crop: @planting.crop, owner: @planting.owner) @matching_seeds ||= Seed.where(crop: @planting.crop, owner: @planting.owner)
.where('(finished_at IS NULL OR finished_at >= ?)', @planting.planted_at) .where('(finished_at IS NULL OR finished_at >= ?)', @planting.planted_at)
.where('(saved_at IS NULL OR saved_at <= ?)', @planting.planted_at) .where('(saved_at IS NULL OR saved_at <= ?)', @planting.planted_at)
end end

View File

@@ -8,7 +8,7 @@ class PostsController < ApplicationController
respond_to :rss, only: %i(index show) respond_to :rss, only: %i(index show)
def index def index
@author = Member.find_by(slug: params[:member_slug]) @author = Member.find_by!(slug: params[:member_slug]) if params[:member_slug].present?
@posts = posts @posts = posts
respond_with(@posts) respond_with(@posts)
end end
@@ -21,6 +21,10 @@ class PostsController < ApplicationController
def new def new
@post = Post.new @post = Post.new
@forum = Forum.find_by(id: params[:forum_id]) @forum = Forum.find_by(id: params[:forum_id])
if params[:crop_id]
@crop = Crop.friendly.find(params[:crop_id])
@post.body = "[#{@crop.name}](crop)"
end
respond_with(@post) respond_with(@post)
end end
@@ -29,17 +33,17 @@ class PostsController < ApplicationController
def create def create
params[:post][:author_id] = current_member.id params[:post][:author_id] = current_member.id
@post = Post.new(post_params) @post = Post.new(post_params)
flash[:notice] = 'Post was successfully created.' if @post.save flash[:notice] = t('posts.created') if @post.save
respond_with(@post) respond_with(@post)
end end
def update def update
flash[:notice] = 'Post was successfully updated.' if @post.update(post_params) flash[:notice] = t('posts.updated') if @post.update(post_params)
respond_with(@post) respond_with(@post)
end end
def destroy def destroy
flash[:notice] = 'Post was deleted.' if @post.destroy flash[:notice] = t('posts.deleted') if @post.destroy
respond_with(@post) respond_with(@post)
end end

View File

@@ -54,7 +54,7 @@ class ScientificNamesController < ApplicationController
def destroy def destroy
@crop = @scientific_name.crop @crop = @scientific_name.crop
@scientific_name.destroy @scientific_name.destroy
flash[:notice] = 'Scientific name was successfully deleted.' flash[:notice] = t('scientific_names.deleted')
respond_with(@crop) respond_with(@crop)
end end

View File

@@ -5,7 +5,7 @@ class SeedsController < DataController
where = {} where = {}
if params[:member_slug].present? if params[:member_slug].present?
@owner = Member.find_by(slug: params[:member_slug]) @owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id where['owner_id'] = @owner.id
end end
@@ -30,7 +30,7 @@ class SeedsController < DataController
page: params[:page], page: params[:page],
limit: 30, limit: 30,
boost_by: [:created_at], boost_by: [:created_at],
load: false load: (request.format.csv? ? { include: %i(crop owner) } : false)
) )
respond_with(@seeds) respond_with(@seeds)
@@ -61,7 +61,7 @@ class SeedsController < DataController
@seed.finished ||= false @seed.finished ||= false
@seed.owner = current_member @seed.owner = current_member
@seed.crop = @seed.parent_planting.crop if @seed.parent_planting @seed.crop = @seed.parent_planting.crop if @seed.parent_planting
flash[:notice] = "Successfully added #{@seed.crop} seed to your stash." if @seed.save flash[:notice] = t('seeds.added_to_stash', crop: @seed.crop) if @seed.save
if params[:return] == 'planting' if params[:return] == 'planting'
respond_with(@seed, location: @seed.parent_planting) respond_with(@seed, location: @seed.parent_planting)
else else
@@ -70,7 +70,7 @@ class SeedsController < DataController
end end
def update def update
flash[:notice] = 'Seed was successfully updated.' if @seed.update(seed_params) flash[:notice] = t('seeds.updated') if @seed.update(seed_params)
respond_with(@seed) respond_with(@seed)
end end

View File

@@ -5,7 +5,7 @@ class SessionsController < Devise::SessionsController
def create def create
super do |_resource| super do |_resource|
flash[:alert] = "There are crops waiting to be wrangled." if Crop.pending_approval.present? && current_member.role?(:crop_wrangler) flash[:alert] = t('messages.crops_waiting') if Crop.pending_approval.present? && current_member.role?(:crop_wrangler)
end end
end end
end end

View File

@@ -2,28 +2,47 @@
module CropsHelper module CropsHelper
def crop_or_parent(crop, attribute) def crop_or_parent(crop, attribute)
default = crop.send(attribute) @crop_or_parent_cache ||= {}
return default if default.present? cache_key = [crop.persisted? ? crop.id : crop.object_id, attribute]
return @crop_or_parent_cache[cache_key] if @crop_or_parent_cache.key?(cache_key)
parent = crop @crop_or_parent_cache[cache_key] = begin
while parent = parent.parent value = crop.send(attribute)
return parent.send(attribute) if parent&.send(attribute).present? if value.blank?
parent = crop
while (parent = parent.parent)
parent_value = parent.send(attribute)
if parent_value.present?
value = parent_value
break
end
end
end
value
end end
# For scopes, arrays, etc return the empty value
default
end end
def display_seed_availability(member, crop) def display_seed_availability(member, crop)
seeds = member.seeds.where(crop:) @seed_availability_cache ||= {}
total_quantity = seeds.where.not(quantity: nil).sum(:quantity) cache_key = [
member.persisted? ? member.id : member.object_id,
crop.persisted? ? crop.id : crop.object_id
]
return @seed_availability_cache[cache_key] if @seed_availability_cache.key?(cache_key)
return "You don't have any seeds of this crop." if seeds.none? @seed_availability_cache[cache_key] = begin
seeds = member.seeds.where(crop:)
if total_quantity == 0 if seeds.none?
"You have an unknown quantity of seeds of this crop." "You don't have any seeds of this crop."
else else
"You have #{total_quantity} #{Seed.model_name.human(count: total_quantity)} of this crop." total_quantity = seeds.where.not(quantity: nil).sum(:quantity)
if total_quantity == 0
"You have an unknown quantity of seeds of this crop."
else
"You have #{total_quantity} #{Seed.model_name.human(count: total_quantity)} of this crop."
end
end
end end
end end
@@ -40,53 +59,57 @@ module CropsHelper
end end
def crop_jsonld_data(crop, full_attributes: true) def crop_jsonld_data(crop, full_attributes: true)
same_as_urls = [crop.en_wikipedia_url] Rails.cache.fetch([crop.cache_key_with_version, "jsonld", full_attributes]) do
crop.scientific_names.each do |scientific_name| same_as_urls = [crop.en_wikipedia_url]
same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}" if scientific_name.wikidata_id.present? crop.scientific_names.each do |scientific_name|
end if scientific_name.wikidata_id.present?
same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}"
subject_of_entities = [] end
if full_attributes
if crop.en_youtube_url.present?
subject_of_entities << {
'@type': "VideoObject",
url: crop.en_youtube_url
}
end
crop.posts.each do |post|
subject_of_entities << {
'@type': "SocialMediaPosting",
url: post_url(post),
author: {
'@type': 'Person',
name: post.author.login_name
},
'datePublished': post.created_at
}
end end
subject_of_entities = []
images = [] images = []
crop.photos.each do |photo| if full_attributes
images << photo.fullsize_url if crop.en_youtube_url.present?
subject_of_entities << {
'@type': "VideoObject",
url: crop.en_youtube_url
}
end
crop.posts.each do |post|
subject_of_entities << {
'@type': "SocialMediaPosting",
url: post_url(post),
author: {
'@type': 'Person',
name: post.author.login_name
},
datePublished: post.created_at
}
end
crop.photos.each do |photo|
images << photo.fullsize_url
end
end end
# TODO: Review plantings, seeds, harvests as a subtype of social media post or event that ended? Or creative work?
# has_many :plantings, dependent: :destroy
# has_many :seeds, dependent: :destroy
# has_many :harvests, dependent: :destroy
{
'@context': "https://schema.org",
'@type': "BioChemEntity",
name: crop.name,
taxonomicRange: crop.scientific_names.map(&:name),
description: crop.description,
sameAs: same_as_urls,
alternateName: crop.alternate_names.map(&:name),
subjectOf: subject_of_entities,
image: images
}.compact
end end
# TODO: Review plantings, seeds, harvests as a subtype of social media post or event that ended? Or creative work?
# has_many :plantings, dependent: :destroy
# has_many :seeds, dependent: :destroy
# has_many :harvests, dependent: :destroy
{
'@context': "https://schema.org",
'@type': "BioChemEntity",
name: crop.name,
taxonomicRange: crop.scientific_names.map(&:name),
description: crop.description,
sameAs: same_as_urls,
alternateName: crop.alternate_names.map(&:name),
subjectOf: subject_of_entities,
image: images
}.compact
end end
end end

View File

@@ -46,9 +46,13 @@ module PlantingsHelper
# Returns a list of gardens the planting can be transplanted to # Returns a list of gardens the planting can be transplanted to
# based on the planting's owner. # based on the planting's owner.
def transplantable_gardens_by_owner(planting) def transplantable_gardens_by_owner(planting)
garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a @transplantable_gardens ||= {}
cache_key = planting.id || planting.object_id
@transplantable_gardens[cache_key] ||= begin
garden_ids = planting.owner.gardens.select(:id).to_a + GardenCollaborator.where(member_id: planting.owner.id).select(:garden_id).to_a
Garden.active.where.not(id: planting.garden_id).where(id: garden_ids) Garden.active.where.not(id: planting.garden_id).where(id: garden_ids)
end
end end
def days_from_now_to_last_harvest(planting) def days_from_now_to_last_harvest(planting)

View File

@@ -57,6 +57,19 @@ class NotifierMailer < ApplicationMailer
mail(to: @member.email, subject: @subject) if @member.send_planting_reminder mail(to: @member.email, subject: @subject) if @member.send_planting_reminder
end end
def harvest_reminder(member)
@member = member
@plantings = @member.plantings.active.select(&:harvest_in_next_week?)
@sitename = ENV.fetch('GROWSTUFF_SITE_NAME', nil)
@subject = I18n.t('notifier_mailer.harvest_reminder.subject', sitename: @sitename)
# Encrypting
message = { member_id: @member.id, type: :send_harvest_reminder }
@signed_message = verifier.generate(message)
mail(to: @member.email, subject: @subject) if @member.send_harvest_reminder
end
def new_crop_request(member, request) def new_crop_request(member, request)
@member = member @member = member
@request = request @request = request

View File

@@ -123,6 +123,7 @@ class Ability
can :create, GardenCollaborator, garden: { owner_id: member.id } can :create, GardenCollaborator, garden: { owner_id: member.id }
can :update, GardenCollaborator, garden: { owner_id: member.id } can :update, GardenCollaborator, garden: { owner_id: member.id }
can :destroy, GardenCollaborator, garden: { owner_id: member.id } can :destroy, GardenCollaborator, garden: { owner_id: member.id }
can :destroy, GardenCollaborator, member_id: member.id
can :create, Activity can :create, Activity
can :update, Activity, owner_id: member.id can :update, Activity, owner_id: member.id
@@ -164,6 +165,12 @@ class Ability
can :destroy, Follow can :destroy, Follow
cannot :destroy, Follow, followed_id: member.id # can't unfollow yourself cannot :destroy, Follow, followed_id: member.id # can't unfollow yourself
# blocking/unblocking permissions
can :create, Block
cannot :create, Block, blocked_id: member.id # can't block yourself
can :destroy, Block, blocker_id: member.id # can only unblock your own blocks
cannot :create, GardenType cannot :create, GardenType
cannot :update, GardenType cannot :update, GardenType
cannot :destroy, GardenType cannot :destroy, GardenType

View File

@@ -4,7 +4,6 @@ class Activity < ApplicationRecord
extend FriendlyId extend FriendlyId
include Ownable include Ownable
include Finishable include Finishable
include SearchActivities
include Likeable include Likeable
belongs_to :garden, optional: true belongs_to :garden, optional: true
@@ -46,4 +45,17 @@ class Activity < ApplicationRecord
def planting_slug def planting_slug
planting&.crop&.slug planting&.crop&.slug
end end
scope :active, -> { where(finished: [false, nil]) }
def self.homepage_records(limit)
# Get the latest activity for each owner, then return the latest 'limit' of those
Activity.where(id: Activity.unscoped.select("DISTINCT ON (owner_id) id").order("owner_id, created_at DESC"))
.order(created_at: :desc)
.limit(limit)
end
def self.reindex(refresh: false); end
def reindex(refresh: false); end
end end

18
app/models/block.rb Normal file
View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
class Block < ApplicationRecord
belongs_to :blocker, class_name: "Member"
belongs_to :blocked, class_name: "Member"
validates :blocker_id, uniqueness: { scope: :blocked_id }
after_create :destroy_follow_relationship
private
def destroy_follow_relationship
# Destroy the follow relationship in both directions
Follow.where(follower: blocker, followed: blocked).destroy_all
Follow.where(follower: blocked, followed: blocker).destroy_all
end
end

View File

@@ -4,6 +4,7 @@ class Comment < ApplicationRecord
belongs_to :author, class_name: 'Member', inverse_of: :comments belongs_to :author, class_name: 'Member', inverse_of: :comments
belongs_to :commentable, polymorphic: true, counter_cache: true belongs_to :commentable, polymorphic: true, counter_cache: true
# validates :body, presence: true # validates :body, presence: true
validate :author_is_not_blocked
scope :post_order, -> { order(created_at: :asc) } # for display on post page scope :post_order, -> { order(created_at: :asc) } # for display on post page
@@ -25,4 +26,14 @@ class Comment < ApplicationRecord
def to_s def to_s
"#{author.login_name} commented on #{commentable.subject}" "#{author.login_name} commented on #{commentable.subject}"
end end
private
def author_is_not_blocked
return unless author
return unless commentable.author.already_blocking?(author)
errors.add(:base, "You cannot comment on a post of a member who has blocked you.")
end
end end

View File

@@ -40,8 +40,15 @@ module MemberFlickr
# Fetches a collection of photos from Flickr # Fetches a collection of photos from Flickr
# Returns a [[page of photos], total] pair. # Returns a [[page of photos], total] pair.
# Total is needed for pagination. # Total is needed for pagination.
def flickr_photos(page_num = 1, set = nil) def flickr_photos(page_num = 1, set = nil, tags = nil)
result = if set result = if tags.present?
flickr.photos.search(
user_id: 'me',
tags: tags,
page: page_num,
per_page: 30
)
elsif set.present?
flickr.photosets.getPhotos( flickr.photosets.getPhotos(
photoset_id: set, photoset_id: set,
page: page_num, page: page_num,

View File

@@ -6,23 +6,31 @@ module PredictHarvest
included do included do
# dates # dates
def first_harvest_date def first_harvest_date
harvests_with_dates.minimum(:harvested_at) return @first_harvest_date if defined?(@first_harvest_date)
@first_harvest_date = harvests_with_dates.minimum(:harvested_at)
end end
def last_harvest_date def last_harvest_date
harvests_with_dates.maximum(:harvested_at) return @last_harvest_date if defined?(@last_harvest_date)
@last_harvest_date = harvests_with_dates.maximum(:harvested_at)
end end
def first_harvest_predicted_at def first_harvest_predicted_at
return unless crop.median_days_to_first_harvest.present? && planted_at.present? return @first_harvest_predicted_at if defined?(@first_harvest_predicted_at)
planted_at + crop.median_days_to_first_harvest.days @first_harvest_predicted_at = if crop.median_days_to_first_harvest.present? && planted_at.present?
planted_at + crop.median_days_to_first_harvest.days
end
end end
def last_harvest_predicted_at def last_harvest_predicted_at
return unless crop.median_days_to_last_harvest.present? && planted_at.present? return @last_harvest_predicted_at if defined?(@last_harvest_predicted_at)
planted_at + crop.median_days_to_last_harvest.days @last_harvest_predicted_at = if crop.median_days_to_last_harvest.present? && planted_at.present?
planted_at + crop.median_days_to_last_harvest.days
end
end end
# actions # actions
@@ -52,10 +60,16 @@ module PredictHarvest
def before_harvest_time? def before_harvest_time?
first_harvest_predicted_at.present? && first_harvest_predicted_at.present? &&
harvests.empty? && harvests.empty? &&
first_harvest_predicted_at.present? &&
first_harvest_predicted_at > Time.zone.today first_harvest_predicted_at > Time.zone.today
end end
def harvest_in_next_week?
first_harvest_predicted_at.present? &&
harvests.empty? &&
first_harvest_predicted_at >= Time.zone.today &&
first_harvest_predicted_at <= Time.zone.today + 7.days
end
def harvest_months def harvest_months
Rails.cache.fetch("#{cache_key_with_version}/harvest_months", expires_in: 5.minutes) do Rails.cache.fetch("#{cache_key_with_version}/harvest_months", expires_in: 5.minutes) do
neighbours_for_harvest_predictions.where.not(harvested_at: nil) neighbours_for_harvest_predictions.where.not(harvested_at: nil)
@@ -65,16 +79,18 @@ module PredictHarvest
end end
def neighbours_for_harvest_predictions def neighbours_for_harvest_predictions
# use this planting's harvest if any @neighbours_for_harvest_predictions ||= begin
return harvests if harvests.size.positive? # use this planting's harvest if any
if harvests.size.positive?
# otherwise use nearby plantings harvests
if location # otherwise use nearby plantings
return Harvest.where(planting: nearby_same_crop.has_harvests) elsif location
.where.not(planting_id: nil) Harvest.where(planting: nearby_same_crop.has_harvests)
.where.not(planting_id: nil)
else
Harvest.none
end
end end
Harvest.none
end end
private private

View File

@@ -13,40 +13,49 @@ module PredictPlanting
# dates # dates
def finish_predicted_at def finish_predicted_at
if planted_at.blank? || failed? return @finish_predicted_at if defined?(@finish_predicted_at)
nil
elsif crop.median_lifespan.present? @finish_predicted_at = if planted_at.blank? || failed?
planted_at + crop.median_lifespan.days nil
elsif crop.parent.present? && crop.parent.median_lifespan.present? elsif crop.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days planted_at + crop.median_lifespan.days
end elsif crop.parent.present? && crop.parent.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days
end
end end
# days # days
def expected_lifespan def expected_lifespan
if actual_lifespan.present? return @expected_lifespan if defined?(@expected_lifespan)
actual_lifespan
elsif crop.median_lifespan.present? @expected_lifespan = if actual_lifespan.present?
crop.median_lifespan actual_lifespan
elsif crop.parent.present? && crop.parent.median_lifespan.present? elsif crop.median_lifespan.present?
crop.parent.median_lifespan crop.median_lifespan
end elsif crop.parent.present? && crop.parent.median_lifespan.present?
crop.parent.median_lifespan
end
end end
def actual_lifespan def actual_lifespan
return unless planted_at.present? && finished_at.present? && !failed? return @actual_lifespan if defined?(@actual_lifespan)
(finished_at - planted_at).to_i @actual_lifespan = if planted_at.present? && finished_at.present? && !failed?
(finished_at - planted_at).to_i
end
end end
def age_in_days def age_in_days
return if planted_at.blank? return @age_in_days if defined?(@age_in_days)
return if failed?
known_last_day ||= finished_at || Time.zone.today @age_in_days = if planted_at.blank? || failed?
known_last_day = Time.zone.today if known_last_day > Time.zone.today nil
else
known_last_day = finished_at || Time.zone.today
known_last_day = Time.zone.today if known_last_day > Time.zone.today
(known_last_day - planted_at).to_i (known_last_day - planted_at).to_i
end
end end
def percentage_grown def percentage_grown

View File

@@ -1,66 +0,0 @@
# frozen_string_literal: true
module SearchActivities
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true,
settings: { number_of_shards: 1, number_of_replicas: 0 },
mappings: {
properties: {
active: { type: :boolean },
created_at: { type: :integer },
updated_at: { type: :integer },
due_date: { type: :date }
}
}
def search_data
{
slug:,
active:,
finished: finished?,
name:,
due_date:,
category:,
garden_id:,
garden_name: garden&.name,
garden_slug: garden&.garden_slug,
planting_id:,
planting_name: planting&.crop&.name,
planting_slug: planting&.slug,
description:,
# owner
owner_id:,
owner_login_name:,
owner_slug:,
# timestamps
created_at: created_at.to_i,
updated_at: updated_at.to_i
}
end
def self.homepage_records(limit)
records = []
owners = []
1..limit.times do
where = {
# photos_count: { gt: 0 },
owner_id: { not: owners }
}
one_record = search('*',
limit: 1,
where:,
boost_by: [:created_at],
load: false).first
return records if one_record.nil?
owners << one_record.owner_id
records << one_record
end
records
end
end
end

View File

@@ -57,13 +57,13 @@ class Crop < ApplicationRecord
validates :en_wikipedia_url, validates :en_wikipedia_url,
format: { format: {
with: %r{\Ahttps?://en\.wikipedia\.org/wiki/[[:alnum:]%_.()-]+\z}, with: %r{\Ahttps?://en\.wikipedia\.org/wiki/[[:alnum:]%_.()-]+\z},
message: 'is not a valid English Wikipedia URL' message: :not_a_valid_wikipedia_url
}, },
if: :approved? if: :approved?
validates :en_youtube_url, validates :en_youtube_url,
format: { format: {
with: %r{\A(?:https?://)?(?:www\.)?(?:youtube(?:-nocookie)?\.com/(?:(?:v|e(?:mbed)?)/|\S*?[?&]v=)|youtu\.be/)[a-zA-Z0-9_-]{11}(?:[?&]\S*)?\z}, with: %r{\A(?:https?://)?(?:www\.)?(?:youtube(?:-nocookie)?\.com/(?:(?:v|e(?:mbed)?)/|\S*?[?&]v=)|youtu\.be/)[a-zA-Z0-9_-]{11}(?:[?&]\S*)?\z},
message: 'is not a valid YouTube URL' message: :not_a_valid_youtube_url
}, },
allow_blank: true allow_blank: true
validates :name, uniqueness: { scope: :approval_status }, if: :pending? validates :name, uniqueness: { scope: :approval_status }, if: :pending?
@@ -165,9 +165,11 @@ class Crop < ApplicationRecord
end end
def all_companions def all_companions
return companions unless parent @all_companions ||= if parent
(companions + parent.all_companions).uniq
(companions + parent.all_companions).uniq else
companions
end
end end
before_destroy :destroy_reverse_companionships before_destroy :destroy_reverse_companionships
@@ -190,12 +192,12 @@ class Crop < ApplicationRecord
return if rejected? return if rejected?
return unless reason_for_rejection.present? || rejection_notes.present? return unless reason_for_rejection.present? || rejection_notes.present?
errors.add(:approval_status, "must be rejected if a reason for rejection is present") errors.add(:approval_status, :rejection_reason_required)
end end
def must_have_meaningful_reason_for_rejection def must_have_meaningful_reason_for_rejection
return unless reason_for_rejection == "other" && rejection_notes.blank? return unless reason_for_rejection == "other" && rejection_notes.blank?
errors.add(:rejection_notes, "must be added if the reason for rejection is \"other\"") errors.add(:rejection_notes, :rejection_notes_required)
end end
end end

View File

@@ -4,6 +4,7 @@ class Follow < ApplicationRecord
belongs_to :follower, class_name: "Member", inverse_of: :follows belongs_to :follower, class_name: "Member", inverse_of: :follows
belongs_to :followed, class_name: "Member", inverse_of: :inverse_follows belongs_to :followed, class_name: "Member", inverse_of: :inverse_follows
validates :follower_id, uniqueness: { scope: :followed_id } validates :follower_id, uniqueness: { scope: :followed_id }
validate :follower_is_not_blocked
after_create do after_create do
Notification.create( Notification.create(
@@ -14,4 +15,14 @@ class Follow < ApplicationRecord
notifiable: self notifiable: self
) )
end end
private
def follower_is_not_blocked
return unless follower
return unless followed.already_blocking?(follower)
errors.add(:base, "You cannot follow a member who has blocked you.")
end
end end

View File

@@ -21,6 +21,7 @@ class Garden < ApplicationRecord
after_validation :cleanup_area after_validation :cleanup_area
after_validation :geocode after_validation :geocode
after_validation :empty_unwanted_geocodes after_validation :empty_unwanted_geocodes
after_validation :populate_wikidata_info, if: :will_save_change_to_location?
after_save :mark_inactive_garden_plantings_as_finished after_save :mark_inactive_garden_plantings_as_finished
scope :active, -> { where(active: true) } scope :active, -> { where(active: true) }
@@ -32,7 +33,7 @@ class Garden < ApplicationRecord
validates :name, uniqueness: { scope: :owner_id } validates :name, uniqueness: { scope: :owner_id }
validates :name, validates :name,
format: { without: /\n/, message: "must contain no newlines" }, format: { without: /\n/, message: :no_newlines },
allow_blank: false, presence: true, allow_blank: false, presence: true,
length: { maximum: 255 } length: { maximum: 255 }
@@ -53,7 +54,7 @@ class Garden < ApplicationRecord
"acres" => "acre" "acres" => "acre"
}.freeze }.freeze
validates :area_unit, inclusion: { in: AREA_UNITS_VALUES.values, validates :area_unit, inclusion: { in: AREA_UNITS_VALUES.values,
message: "%<value>s is not a valid area unit" }, message: :not_a_valid_area_unit },
allow_blank: true allow_blank: true
def cleanup_area def cleanup_area
@@ -92,6 +93,19 @@ class Garden < ApplicationRecord
end end
end end
def populate_wikidata_info
return false if location.blank?
wd_id = WikidataService.find_wikidata_id(location)
return false if wd_id.blank?
self.location_wikidata_id = wd_id
temps = WikidataService.fetch_temps(wd_id)
self.highest_temp_c = temps[:highest_temp_c]
self.lowest_temp_c = temps[:lowest_temp_c]
true
end
protected protected
def strip_blanks def strip_blanks

View File

@@ -11,7 +11,7 @@ class GardenCollaborator < ApplicationRecord
return unless member return unless member
return unless garden return unless garden
errors.add(:member_id, "cannot be the garden owner") if garden.owner == member errors.add(:member_id, :cannot_be_garden_owner) if garden.owner == member
end end
def member_slug def member_slug

View File

@@ -8,6 +8,8 @@ class Harvest < ApplicationRecord
include SearchHarvests include SearchHarvests
include Likeable include Likeable
attr_accessor :overall_rating
friendly_id :harvest_slug, use: %i(slugged finders) friendly_id :harvest_slug, use: %i(slugged finders)
# Constants # Constants
@@ -58,18 +60,18 @@ class Harvest < ApplicationRecord
## ##
## Validations ## Validations
validates :crop, approved: true validates :crop, approved: true
validates :crop, presence: { message: "must be present and exist in our database" } validates :crop, presence: { message: :crop_not_found }
validates :plant_part, presence: { message: "must be present and exist in our database" } validates :plant_part, presence: { message: :crop_not_found }
validates :harvested_at, presence: true validates :harvested_at, presence: true
validates :quantity, allow_nil: true, numericality: { validates :quantity, allow_nil: true, numericality: {
only_integer: false, greater_than_or_equal_to: 0 only_integer: false, greater_than_or_equal_to: 0
} }
validates :unit, allow_blank: true, inclusion: { validates :unit, allow_blank: true, inclusion: {
in: UNITS_VALUES.values, message: "%<value>s is not a valid unit" in: UNITS_VALUES.values, message: :not_a_valid_unit
} }
validates :weight_quantity, allow_nil: true, numericality: { only_integer: false } validates :weight_quantity, allow_nil: true, numericality: { only_integer: false }
validates :weight_unit, allow_blank: true, inclusion: { validates :weight_unit, allow_blank: true, inclusion: {
in: WEIGHT_UNITS_VALUES.values, message: "%<value>s is not a valid unit" in: WEIGHT_UNITS_VALUES.values, message: :not_a_valid_unit
} }
validate :crop_must_match_planting validate :crop_must_match_planting
validate :owner_must_match_planting validate :owner_must_match_planting
@@ -109,37 +111,49 @@ class Harvest < ApplicationRecord
def to_s def to_s
# 50 individual apples, weighing 3lb # 50 individual apples, weighing 3lb
# 2 buckets of apricots, weighing 10kg # 2 buckets of apricots, weighing 10kg
"#{quantity_to_human} #{unit_to_human} #{crop_name_to_human} #{weight_to_human}".strip @to_s ||= "#{quantity_to_human} #{unit_to_human} #{crop_name_to_human} #{weight_to_human}".strip
end end
def quantity_to_human def quantity_to_human
return number_to_human(quantity.to_s, strip_insignificant_zeros: true) if quantity @quantity_to_human ||= if quantity
number_to_human(quantity.to_s, strip_insignificant_zeros: true)
"" else
""
end
end end
def unit_to_human def unit_to_human
return "" unless quantity && unit @unit_to_human ||= begin
return 'individual' if unit == 'individual' if !quantity || !unit
return "#{unit} of" if quantity == 1 ""
elsif unit == 'individual'
"#{unit.pluralize} of" 'individual'
elsif quantity == 1
"#{unit} of"
else
"#{unit.pluralize} of"
end
end
end end
def weight_to_human def weight_to_human
return "" unless weight_quantity @weight_to_human ||= if weight_quantity
"weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}"
"weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}" else
""
end
end end
def crop_name_to_human def crop_name_to_human
if unit != 'individual' # buckets of apricot*s* @crop_name_to_human ||= begin
crop.name.pluralize if unit != 'individual' # buckets of apricot*s*
elsif quantity == 1 crop.name.pluralize
crop.name elsif quantity == 1
else crop.name
crop.name.pluralize else
end.to_s crop.name.pluralize
end.to_s
end
end end
private private
@@ -147,7 +161,7 @@ class Harvest < ApplicationRecord
def crop_must_match_planting def crop_must_match_planting
return if planting.blank? # only check if we are linked to a planting return if planting.blank? # only check if we are linked to a planting
errors.add(:planting, "must be the same crop") unless crop == planting.crop errors.add(:planting, :same_crop_required) unless crop == planting.crop
end end
def owner_must_match_planting def owner_must_match_planting
@@ -155,14 +169,13 @@ class Harvest < ApplicationRecord
return if owner == planting.owner || planting.garden.garden_collaborators.where(member_id: owner).any? return if owner == planting.owner || planting.garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner, errors.add(:owner, :same_owner_required)
"of harvest must be the same as planting, or a collaborator on that garden")
end end
def harvest_must_be_after_planting def harvest_must_be_after_planting
# only check if we are linked to a planting # only check if we are linked to a planting
return unless harvested_at.present? && planting.present? && planting.planted_at.present? return unless harvested_at.present? && planting.present? && planting.planted_at.present?
errors.add(:planting, "cannot be harvested before planting") unless harvested_at > planting.planted_at errors.add(:planting, :harvest_after_planted) unless harvested_at > planting.planted_at
end end
end end

View File

@@ -5,4 +5,24 @@ class Like < ApplicationRecord
belongs_to :likeable, polymorphic: true, counter_cache: true, touch: true belongs_to :likeable, polymorphic: true, counter_cache: true, touch: true
validates :member, :likeable, presence: true validates :member, :likeable, presence: true
validates :member, uniqueness: { scope: :likeable } validates :member, uniqueness: { scope: :likeable }
validate :member_is_not_blocked
def likeable_author
if likeable.respond_to?(:author)
likeable.author
elsif likeable.respond_to?(:owner)
likeable.owner
end
end
private
def member_is_not_blocked
return unless member
author = likeable_author
return unless author&.already_blocking?(member)
errors.add(:base, "You cannot like content of a member who has blocked you.")
end
end end

View File

@@ -52,6 +52,15 @@ class Member < ApplicationRecord
has_many :followed, through: :follows has_many :followed, through: :follows
has_many :followers, through: :inverse_follows, source: :follower has_many :followers, through: :inverse_follows, source: :follower
#
# Blocking other members
has_many :blocks, class_name: "Block", foreign_key: "blocker_id", dependent: :destroy,
inverse_of: :blocker
has_many :inverse_blocks, class_name: "Block", foreign_key: "blocked_id",
dependent: :destroy, inverse_of: :blocked
has_many :blocked_members, through: :blocks, source: :blocked
has_many :blockers, through: :inverse_blocks, source: :blocker
# #
# Global data records this member created # Global data records this member created
has_many :requested_crops, class_name: 'Crop', foreign_key: 'requester_id', dependent: :nullify, has_many :requested_crops, class_name: 'Crop', foreign_key: 'requester_id', dependent: :nullify,
@@ -70,6 +79,7 @@ class Member < ApplicationRecord
scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings } scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings }
scope :has_plantings, -> { joins(:plantings).group("members.id") } scope :has_plantings, -> { joins(:plantings).group("members.id") }
scope :wants_reminders, -> { where(send_planting_reminder: true) } scope :wants_reminders, -> { where(send_planting_reminder: true) }
scope :wants_harvest_reminders, -> { where(send_harvest_reminder: true) }
# Include default devise modules. Others available are: # Include default devise modules. Others available are:
# :token_authenticatable, :confirmable, # :token_authenticatable, :confirmable,
@@ -96,21 +106,21 @@ class Member < ApplicationRecord
validates :tos_agreement, acceptance: { allow_nil: true, accept: true } validates :tos_agreement, acceptance: { allow_nil: true, accept: true }
validates :login_name, validates :login_name,
length: { length: {
minimum: 2, maximum: 25, message: "should be between 2 and 25 characters long" minimum: 2, maximum: 25, message: :login_name_length
}, },
exclusion: { exclusion: {
in: %w(growstuff admin moderator staff nearby), message: "name is reserved" in: %w(growstuff admin moderator staff nearby), message: :login_name_reserved
}, },
format: { format: {
with: /\A\w+\z/, message: "may only include letters, numbers, or underscores" with: /\A\w+\z/, message: :login_name_format
}, },
uniqueness: { uniqueness: {
case_sensitive: false case_sensitive: false
} }
validates :website_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }, allow_blank: true validates :website_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
validates :other_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }, allow_blank: true validates :other_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
validates :instagram_handle, :facebook_handle, :bluesky_handle, validates :instagram_handle, :facebook_handle, :bluesky_handle,
format: { without: %r{\Ahttps?://|/}, message: "should be a handle, not a URL" }, allow_blank: true format: { without: %r{\Ahttps?://|/}, message: :handle_format }, allow_blank: true
# #
# Triggers # Triggers
@@ -152,7 +162,7 @@ class Member < ApplicationRecord
end end
def unread_count def unread_count
receipts.where(is_read: false).count @unread_count ||= receipts.where(is_read: false).count
end end
def self.login_name_or_email(login) def self.login_name_or_email(login)
@@ -164,12 +174,12 @@ class Member < ApplicationRecord
end end
def self.nearest_to(place) def self.nearest_to(place)
nearby_members = [] return [] if place.blank?
if place
latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 }) latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 })
nearby_members = Member.located.sort_by { |x| x.distance_from([latitude, longitude]) } if latitude && longitude return [] unless latitude && longitude
end
nearby_members Member.located.near([latitude, longitude], 1000)
end end
def already_following?(member) def already_following?(member)
@@ -179,4 +189,33 @@ class Member < ApplicationRecord
def get_follow(member) def get_follow(member)
follows.find_by(followed_id: member.id) if already_following?(member) follows.find_by(followed_id: member.id) if already_following?(member)
end end
def already_blocking?(member)
blocks.exists?(blocked_id: member.id)
end
def get_block(member)
blocks.find_by(blocked_id: member.id) if already_blocking?(member)
end
def has_activity?
(gardens.exists? && gardens.count > 1) ||
plantings.exists? ||
harvests.exists? ||
seeds.exists? ||
photos.exists? ||
forums.exists? ||
activities.exists? ||
posts.exists? ||
comments.exists? ||
requested_crops.exists? ||
created_crops.exists? ||
likes.exists? ||
created_alternate_names.exists? ||
created_scientific_names.exists? ||
follows.exists? ||
inverse_follows.exists? ||
blocks.exists? ||
inverse_blocks.exists?
end
end end

View File

@@ -29,12 +29,12 @@ class PhotoAssociation < ApplicationRecord
def photo_and_item_have_same_owner def photo_and_item_have_same_owner
return if photographable_type == 'Crop' return if photographable_type == 'Crop'
errors.add(:photo, "must have same owner as item it links to") unless photographable.owner_id == photo.owner_id errors.add(:photo, :photo_owner_mismatch) unless photographable.owner_id == photo.owner_id
end end
def crop_present def crop_present
return unless %w(Planting Seed Harvest).include?(photographable_type) return unless %w(Planting Seed Harvest).include?(photographable_type)
errors.add(:crop_id, "failed to calculate crop") if crop_id.blank? errors.add(:crop_id, :calculate_crop_failed) if crop_id.blank?
end end
end end

View File

@@ -72,7 +72,7 @@ class Planting < ApplicationRecord
## ##
## Validations ## Validations
validates :garden, presence: true validates :garden, presence: true
validates :crop, presence: true, approved: { message: "must be present and exist in our database" } validates :crop, presence: true, approved: { message: :crop_must_be_approved }
validate :finished_must_be_after_planted validate :finished_must_be_after_planted
validate :owner_must_match_garden_owner validate :owner_must_match_garden_owner
validate :cannot_be_finished_and_failed validate :cannot_be_finished_and_failed
@@ -80,10 +80,10 @@ class Planting < ApplicationRecord
only_integer: true, greater_than_or_equal_to: 0 only_integer: true, greater_than_or_equal_to: 0
} }
validates :sunniness, allow_blank: true, inclusion: { validates :sunniness, allow_blank: true, inclusion: {
in: SUNNINESS_VALUES, message: "%<value>s is not a valid sunniness value" in: SUNNINESS_VALUES, message: :not_a_valid_sunniness
} }
validates :planted_from, allow_blank: true, inclusion: { validates :planted_from, allow_blank: true, inclusion: {
in: PLANTED_FROM_VALUES, message: "%<value>s is not a valid planting method" in: PLANTED_FROM_VALUES, message: :not_a_valid_planting_method
} }
validates :overall_rating, allow_blank: true, numericality: { validates :overall_rating, allow_blank: true, numericality: {
only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5 only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5
@@ -119,33 +119,36 @@ class Planting < ApplicationRecord
end end
def nearby_same_crop def nearby_same_crop
return Planting.none if location.blank? || latitude.blank? || longitude.blank? return @nearby_same_crop if defined?(@nearby_same_crop)
# latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 }) @nearby_same_crop = if location.blank? || latitude.blank? || longitude.blank?
Planting.joins(:garden) Planting.none
.where(crop:) else
.located # latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 })
.where('gardens.latitude < ? AND gardens.latitude > ?', Planting.joins(:garden)
latitude + 10, latitude - 10) .where(crop:)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
end
end end
private private
def cannot_be_finished_and_failed def cannot_be_finished_and_failed
errors.add(:failed, "can't be true if planting is also finished") if finished && failed errors.add(:failed, :failed_and_finished) if finished && failed
end end
# check that any finished_at date occurs after planted_at # check that any finished_at date occurs after planted_at
def finished_must_be_after_planted def finished_must_be_after_planted
return unless planted_at && finished_at # only check if we have both return unless planted_at && finished_at # only check if we have both
errors.add(:finished_at, "must be after the planting date") unless planted_at < finished_at errors.add(:finished_at, :finished_after_planted) unless planted_at < finished_at
end end
def owner_must_match_garden_owner def owner_must_match_garden_owner
return if owner == garden.owner || garden.garden_collaborators.where(member_id: owner).any? return if owner == garden.owner || garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner, errors.add(:owner, :same_owner_required)
"must be the same as garden, or a collaborator on that garden")
end end
end end

View File

@@ -49,9 +49,10 @@ class Post < ApplicationRecord
# return posts sorted by recent activity # return posts sorted by recent activity
def self.recently_active def self.recently_active
Post.order(created_at: :desc).sort do |a, b| left_joins(:comments)
b.recent_activity <=> a.recent_activity .select('posts.*, COALESCE(MAX(comments.created_at), posts.created_at) AS last_activity_at')
end .group('posts.id')
.order(Arel.sql('last_activity_at DESC'))
end end
def owner_id def owner_id

View File

@@ -28,7 +28,7 @@ class Seed < ApplicationRecord
# #
# Validations # Validations
validates :crop, approved: true validates :crop, approved: true
validates :crop, presence: { message: "must be present and exist in our database" } validates :crop, presence: { message: :crop_not_found }
validates :quantity, allow_nil: true, validates :quantity, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :days_until_maturity_min, allow_nil: true, validates :days_until_maturity_min, allow_nil: true,
@@ -36,20 +36,15 @@ class Seed < ApplicationRecord
validates :days_until_maturity_max, allow_nil: true, validates :days_until_maturity_max, allow_nil: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 } numericality: { only_integer: true, greater_than_or_equal_to: 0 }
validates :tradable_to, allow_blank: false, validates :tradable_to, allow_blank: false,
inclusion: { in: TRADABLE_TO_VALUES, message: "You may only trade seed nowhere, " \ inclusion: { in: TRADABLE_TO_VALUES, message: :tradable_to_inclusion }
"locally, nationally, or internationally" }
validates :organic, allow_blank: false, validates :organic, allow_blank: false,
inclusion: { in: ORGANIC_VALUES, message: "You must say whether the seeds " \ inclusion: { in: ORGANIC_VALUES, message: :organic_inclusion }
"are organic or not, or that you don't know" }
validates :gmo, allow_blank: false, validates :gmo, allow_blank: false,
inclusion: { in: GMO_VALUES, message: "You must say whether the seeds are " \ inclusion: { in: GMO_VALUES, message: :gmo_inclusion }
"genetically modified or not, or that you don't know" }
validates :heirloom, allow_blank: false, validates :heirloom, allow_blank: false,
inclusion: { in: HEIRLOOM_VALUES, message: "You must say whether the seeds" \ inclusion: { in: HEIRLOOM_VALUES, message: :heirloom_inclusion }
"are heirloom, hybrid, or unknown" }
validates :source, allow_blank: true, validates :source, allow_blank: true,
inclusion: { in: SOURCE_VALUES, message: "You must say where the seeds are from," \ inclusion: { in: SOURCE_VALUES, message: :source_inclusion }
"or that you don't know" }
# #
# Delegations # Delegations

View File

@@ -2,7 +2,7 @@
class CropSearchService class CropSearchService
# Crop.search(string) # Crop.search(string)
def self.search(query, page: 1, per_page: 12, current_member: nil) def self.search(query, page: 1, per_page: 12, current_member: nil, **options)
search_params = { search_params = {
page:, page:,
per_page:, per_page:,
@@ -12,7 +12,7 @@ class CropSearchService
includes: %i(scientific_names alternate_names), includes: %i(scientific_names alternate_names),
misspellings: { edit_distance: 2 }, misspellings: { edit_distance: 2 },
load: false load: false
} }.merge(options)
# prioritise crops the member has planted # prioritise crops the member has planted
search_params[:boost_where] = { planters_ids: current_member.id } if current_member search_params[:boost_where] = { planters_ids: current_member.id } if current_member

View File

@@ -85,7 +85,7 @@ class GbifService
end end
def import! def import!
Crop.order(updated_at: :desc).each do |crop| Crop.order(updated_at: :desc).find_each do |crop|
Rails.logger.debug { "#{crop.id}, #{crop.name}" } Rails.logger.debug { "#{crop.id}, #{crop.name}" }
update_crop(crop) if crop.valid? update_crop(crop) if crop.valid?
rescue ActiveRecord::RecordInvalid rescue ActiveRecord::RecordInvalid

View File

@@ -0,0 +1,74 @@
# frozen_string_literal: true
require 'net/http'
require 'json'
class WikidataService
CELSIUS_UNIT_ID = 'http://www.wikidata.org/entity/Q25267'
FAHRENHEIT_UNIT_ID = 'http://www.wikidata.org/entity/Q42289'
def self.find_wikidata_id(location_name)
return nil if location_name.blank?
uri = URI("https://www.wikidata.org/w/api.php?action=wbsearchentities&search=#{URI.encode_www_form_component(location_name)}&language=en&format=json")
req = Net::HTTP::Get.new(uri)
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
data = JSON.parse(response.body)
data.dig('search', 0, 'id')
rescue StandardError => e
Rails.logger.error "WikidataService.find_wikidata_id error: #{e.message}"
nil
end
def self.fetch_temps(wikidata_id)
return {} if wikidata_id.blank?
uri = URI("https://www.wikidata.org/w/api.php?action=wbgetentities&ids=#{wikidata_id}&props=claims&format=json")
req = Net::HTTP::Get.new(uri)
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req)
end
data = JSON.parse(response.body)
claims = data.dig('entities', wikidata_id, 'claims') || {}
highest_temp = extract_temp(claims['P6591'])
lowest_temp = extract_temp(claims['P7422'])
{
highest_temp_c: highest_temp,
lowest_temp_c: lowest_temp
}
rescue StandardError => e
Rails.logger.error "WikidataService.fetch_temps error: #{e.message}"
{}
end
def self.extract_temp(claim_data)
return nil if claim_data.blank?
# We take the first value
main_snak = claim_data.first&.dig('mainsnak')
return nil unless main_snak&.dig('datavalue', 'type') == 'quantity'
quantity_data = main_snak.dig('datavalue', 'value')
amount = quantity_data['amount'].to_f
unit = quantity_data['unit']
case unit
when CELSIUS_UNIT_ID
amount
when FAHRENHEIT_UNIT_ID
(amount - 32) * 5.0 / 9.0
else
nil
end
end
end

View File

@@ -3,7 +3,7 @@
- content_for :breadcrumbs do - content_for :breadcrumbs do
- if @owner - if @owner
%li.breadcrumb-item= link_to 'Activities', activities_path %li.breadcrumb-item= link_to 'Activities', activities_path
%li.breadcrumb-item.active= link_to "#{@owner}'s activities", activities_path(owner: @owner) %li.breadcrumb-item.active= link_to "#{@owner}'s activities", member_activities_path(@owner)
- else - else
%li.breadcrumb-item.active= link_to 'Activities', activities_path %li.breadcrumb-item.active= link_to 'Activities', activities_path

View File

@@ -16,14 +16,14 @@
%p %p
%span.help-block %span.help-block
For detailed crop wrangling guidelines, please consult the For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "http://wiki.growstuff.org/index.php/Crop_wrangling" = link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
on the Growstuff wiki. on the Growstuff wiki.
.form-group .form-group
= f.label :crop_id, class: 'control-label col-md-2' = f.label :crop_id, class: 'control-label col-md-2'
.col-md-8 .col-md-8
= collection_select(:alternate_name, :crop_id, = select(:alternate_name, :crop_id,
Crop.all, :id, :name, Crop.order(:name).pluck(:name, :id),
{ selected: @alternate_name.crop_id || @crop.id }, { selected: @alternate_name.crop_id || @crop.id },
class: 'form-control') class: 'form-control')

View File

@@ -1,6 +1,12 @@
- if crop.approved? && signed_in? - if crop.approved? && signed_in?
- active_plantings = current_member.plantings.where(crop: crop).active
.btn-group.crop-actions{"aria-label" => "Crop Actions", role: "group"} .btn-group.crop-actions{"aria-label" => "Crop Actions", role: "group"}
= render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member) = render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member)
= render 'harvests/modal', harvest: Harvest.new(crop: @crop, owner: current_member) = render 'harvests/modal', harvest: Harvest.new(crop: @crop, owner: current_member)
= render 'seeds/modal', seed: Seed.new(crop: @crop, owner: current_member) = render 'seeds/modal', seed: Seed.new(crop: @crop, owner: current_member)
= link_to new_post_path(crop_id: crop.slug), class: 'btn', id: 'post-button' do
= post_icon
Post
- if active_plantings.any?
= render 'plantings/failed_modal', crop: crop, active_plantings: active_plantings

View File

@@ -85,7 +85,7 @@
-# Only crop wranglers see the crop hierarchy (for now) -# Only crop wranglers see the crop hierarchy (for now)
- if can? :wrangle, @crop - if can? :wrangle, @crop
= f.collection_select(:parent_id, Crop.all.order(:name), :id, :name, = f.select(:parent_id, Crop.order(:name).pluck(:name, :id),
{ include_blank: true, label: 'Parent crop'}) { include_blank: true, label: 'Parent crop'})
%span.help-block Optional. For setting up crop hierarchies for varieties etc. %span.help-block Optional. For setting up crop hierarchies for varieties etc.

View File

@@ -5,18 +5,19 @@
%p Nobody has harvested this crop yet. %p Nobody has harvested this crop yet.
- unless crop.harvests.empty? - unless crop.harvests.empty?
%ul.list-group.list-group-flush %ul.list-group.list-group-flush
- Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest| - Rails.cache.fetch([crop, "recent_harvests", Time.zone.today]) do
%li.list-group-item - Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest|
= link_to harvest_path(harvest), class: 'card-link' do %li.list-group-item
= harvest_icon = link_to harvest_path(harvest), class: 'card-link' do
#{harvest.owner} harvested #{display_quantity(harvest)}. = harvest_icon
.float-right= render 'members/location', member: harvest.owner #{harvest.owner} harvested #{display_quantity(harvest)}.
.harvest-timeago .float-right= render 'members/location', member: harvest.owner
%small #{standard_time_distance(harvest.harvested_at, Time.zone.now.to_date)} .harvest-timeago
%small #{standard_time_distance(harvest.harvested_at, Time.zone.now.to_date)}
%li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link' %li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link'
- if crop.approved? - if crop.approved?
- if current_member - if current_member
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(crop_id: crop.id), class: 'btn btn-block' %li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(harvest: { crop_id: crop.id }), class: 'btn btn-block'
- else - else
%li.list-group-item.active %li.list-group-item.active
= icon 'fas', 'user' = icon 'fas', 'user'

View File

@@ -8,6 +8,8 @@
= link_to planting_path(planting), class: 'card-link' do = link_to planting_path(planting), class: 'card-link' do
= planting_icon = planting_icon
= planting = planting
- if can?(:edit, planting)
.float-right= render 'plantings/actions', planting: planting
.float-right= render 'members/location', member: planting.owner .float-right= render 'members/location', member: planting.owner
.card-footer .card-footer
- if crop.approved? - if crop.approved?

View File

@@ -6,7 +6,7 @@
Nobody has posted about #{crop.name.pluralize} yet. Nobody has posted about #{crop.name.pluralize} yet.
%p %p
- if can? :create, Post - if can? :create, Post
= link_to "Post something", new_post_path, class: 'btn btn-default' = link_to "Post something", new_post_path(crop_id: crop.slug), class: 'btn btn-default'
- else - else
= render partial: "shared/signin_signup", = render partial: "shared/signin_signup",
locals: { to: "post your tips and experiences growing #{crop.name.pluralize}" } locals: { to: "post your tips and experiences growing #{crop.name.pluralize}" }

View File

@@ -44,8 +44,10 @@ csv.headers *all_headers
@crops.each do |c| @crops.each do |c|
csv.row c do |csv, crop| csv.row c do |csv, crop|
csv.cells :id, :name, :en_wikipedia_url csv.cell :id, c.id
csv.cell :growstuff_url, crop_url(c) csv.cell :name, c.name
csv.cell :en_wikipedia_url, c.en_wikipedia_url
csv.cell :growstuff_url, crop_url(slug: c.slug)
if c.scientific_names.any? if c.scientific_names.any?
csv.cell :default_scientific_name, c.default_scientific_name csv.cell :default_scientific_name, c.default_scientific_name
@@ -58,10 +60,10 @@ csv.headers *all_headers
end end
csv.cell :plantings_count, c.plantings_count || 0 csv.cell :plantings_count, c.plantings_count || 0
csv.cell :seeds_count, c.seeds.size[] csv.cell :seeds_count, c.seeds.size
csv.cell :harvests_count, c.harvests.size csv.cell :harvests_count, c.harvests.size
# Sunniness # Sunniness
sunniness = c.sunniness sunniness = c.sunniness
sunniness_rec = sunniness.max_by{|k,v| v} sunniness_rec = sunniness.max_by{|k,v| v}
@@ -74,7 +76,7 @@ csv.headers *all_headers
# Planted from # Planted from
planted_from = c.planted_from planted_from = c.planted_from
planted_from_rec = planted_from.max_by{|k,v| v} planted_from_rec = planted_from.max_by{|k,v| v}
if planted_from_rec if planted_from_rec
csv.cell :plant_from_recommendation, planted_from_rec[0] csv.cell :plant_from_recommendation, planted_from_rec[0]
@@ -105,12 +107,11 @@ csv.headers *all_headers
csv.cell col, harvested_plant_parts[pp] || 0 csv.cell col, harvested_plant_parts[pp] || 0
end end
csv.cell :added_by_member_id, c.creator.id csv.cell :added_by_member_id, c.creator&.id
csv.cell :added_by_member_name, c.creator.to_s csv.cell :added_by_member_name, c.creator&.to_s
csv.cell :date_added, c.created_at.to_fs(:db) csv.cell :date_added, c.created_at.to_fs(:db)
csv.cell :last_modified, c.updated_at.to_fs(:db) csv.cell :last_modified, c.updated_at.to_fs(:db)
csv.cell :license, "CC-BY-SA Growstuff http://growstuff.org/" csv.cell :license, "CC-BY-SA Growstuff http://growstuff.org/"
end end
end end

View File

@@ -36,10 +36,11 @@
= cute_icon = cute_icon
= render 'predictions', crop: @crop = render 'predictions', crop: @crop
- if @crop.all_companions.any? - if @crop.all_companions.any?
%section.companions - cache [@crop, 'companions'] do
%h2 Companions %section.companions
- @crop.all_companions.each do |companion| %h2 Companions
= render 'crops/tiny', crop: companion - @crop.all_companions.each do |companion|
= render 'crops/tiny', crop: companion
- if crop_or_parent(@crop, :en_youtube_url).present? - if crop_or_parent(@crop, :en_youtube_url).present?
%section.youtube %section.youtube
@@ -73,10 +74,11 @@
= pie_chart crop_harvested_for_path(@crop, format: :json), legend: "bottom" = pie_chart crop_harvested_for_path(@crop, format: :json), legend: "bottom"
- if @crop.varieties.any? - if @crop.varieties.any?
%section.varieties - cache [@crop, 'varieties'] do
%h2 Varieties %section.varieties
.index-cards %h2 Varieties
= render 'varieties', crop: @crop .index-cards
= render 'varieties', crop: @crop
%section.crop-map %section.crop-map
%h2 %h2
@@ -134,9 +136,11 @@
= render 'harvests', crop: @crop = render 'harvests', crop: @crop
= render 'find_seeds', crop: @crop = render 'find_seeds', crop: @crop
= render 'openfarm_data', crop: @crop - cache [@crop, 'openfarm_data'] do
= render 'openfarm_data', crop: @crop
= render 'nutritional_data', crop: @crop - cache [@crop, 'nutritional_data'] do
= render 'nutritional_data', crop: @crop
= cute_icon = cute_icon
.card .card

View File

@@ -28,6 +28,12 @@
= f.check_box :send_planting_reminder = f.check_box :send_planting_reminder
Receive regular reminders to track your planting and harvesting. Receive regular reminders to track your planting and harvesting.
.form-group
.col-md-offset-2.col-md-8.checkbox
%label
= f.check_box :send_harvest_reminder
Receive regular reminders of upcoming harvests.
.form-group .form-group
.col-md-offset-2.col-md-8.checkbox .col-md-offset-2.col-md-8.checkbox
%label %label

View File

@@ -6,6 +6,7 @@
- @forums.each do |forum| - @forums.each do |forum|
%h2= forum %h2= forum
%p= forum.description
%p %p
= localize_plural(forum.posts, Post) = localize_plural(forum.posts, Post)
| |

View File

@@ -102,6 +102,8 @@
%strong Collaborators: %strong Collaborators:
- if can?(:create, GardenCollaborator.new(garden: @garden)) - if can?(:create, GardenCollaborator.new(garden: @garden))
= link_to "Manage", garden_garden_collaborators_path(@garden) = link_to "Manage", garden_garden_collaborators_path(@garden)
- elsif current_member.present? && (collab = @garden.garden_collaborators.find_by(member: current_member))
= link_to "Leave garden", garden_garden_collaborator_path(@garden, collab), method: :delete, class: 'text-danger', data: { confirm: 'Are you sure you want to leave this garden?' }
- if @garden.garden_collaborators.any? - if @garden.garden_collaborators.any?
%ul %ul
- @garden.garden_collaborators.each do |collabator| - @garden.garden_collaborators.each do |collabator|
@@ -122,6 +124,31 @@
%strong Garden type: %strong Garden type:
= @garden.garden_type.name = @garden.garden_type.name
- if @garden.location_wikidata_id.present?
%hr
%p
%small
Data about this location from
= link_to "wikidata", "https://www.wikidata.org/wiki/#{@garden.location_wikidata_id}", target: '_blank', rel: 'noopener noreferrer'
%p
%strong Highest temperature:
- if @garden.highest_temp_c.present?
= "#{ @garden.highest_temp_c.round(1) }°C"
- else
Not known
%p
%strong Lowest temperature:
- if @garden.lowest_temp_c.present?
= "#{ @garden.lowest_temp_c.round(1) }°C"
- else
Not known
- elsif can?(:edit, @garden) && @garden.location.present?
.alert.alert-info
%p Wikidata information is missing for this location.
= button_to "Fetch Wikidata info", fetch_wikidata_garden_path(@garden), method: :post, class: 'btn btn-info btn-sm'
.card .card
.card-header .card-header
%h4 #{@garden.owner}'s gardens %h4 #{@garden.owner}'s gardens

View File

@@ -52,6 +52,17 @@
= f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control') = f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control')
= f.text_area :description, rows: 6, label: 'Notes' = f.text_area :description, rows: 6, label: 'Notes'
- if @planting.present?
.row
.col-md-12
= f.range_field :overall_rating, min: 1, max: 5, value: @planting.overall_rating, include_blank: 'Leave blank', label: 'Overall Rating - Planting', list: "rating-list", title: "How well is the planting going?"
%datalist{"id": "rating-list"}
%option{"value": "1"} Poor
%option{"value": "2"}
%option{"value": "3"}
%option{"value": "4"}
%option{"value": "5"} Great
.card-footer .card-footer
.text-right= f.submit 'Save' .text-right= f.submit 'Save'

View File

@@ -13,7 +13,7 @@
.index-cards .index-cards
- harvest.crop.plant_parts.order(:name).each do |plant_part| - harvest.crop.plant_parts.order(:name).each do |plant_part|
.card .card
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post do = link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}) do
.card-title.text-center .card-title.text-center
%h3= plant_part_icon(plant_part.name) %h3= plant_part_icon(plant_part.name)
%h3= plant_part.name %h3= plant_part.name
@@ -22,10 +22,12 @@
%h6 All Plant parts %h6 All Plant parts
%nav.nav %nav.nav
- PlantPart.all.order(:name).each do |plant_part| - PlantPart.all.order(:name).each do |plant_part|
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post, class: 'nav-link border' do = link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), class: 'nav-link border' do
= plant_part_icon(plant_part.name) = plant_part_icon(plant_part.name)
= plant_part = plant_part
%a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""} %a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""}
= harvest_icon = harvest_icon
Record harvest Record harvest

View File

@@ -1,3 +1,4 @@
%p - cache harvest do
%small %p
= harvest.harvested_at %small
= harvest.harvested_at

View File

@@ -4,7 +4,7 @@
- content_for :breadcrumbs do - content_for :breadcrumbs do
- if @owner - if @owner
%li.breadcrumb-item= link_to 'Harvests', harvests_path %li.breadcrumb-item= link_to 'Harvests', harvests_path
%li.breadcrumb-item.active= link_to "#{@owner}'s harvests", harvests_path(owner: @owner) %li.breadcrumb-item.active= link_to "#{@owner}'s harvests", member_harvests_path(@owner)
- else - else
%li.breadcrumb-item.active= link_to "Harvests", harvests_path %li.breadcrumb-item.active= link_to "Harvests", harvests_path
.row .row

View File

@@ -1,5 +1,6 @@
%h2= t('.recently_planted') %h2= t('.recently_planted')
- Planting.homepage_records(6).each do |planting| - Planting.homepage_records(6).each do |planting|
- next unless planting['thumbnail_url'].present?
= link_to planting_path(slug: planting['slug']), class: 'list-group-item list-group-item-action flex-column align-items-start' do = link_to planting_path(slug: planting['slug']), class: 'list-group-item list-group-item-action flex-column align-items-start' do
.d-flex.w-100.justify-content-between.homepage--list-item .d-flex.w-100.justify-content-between.homepage--list-item
%p.mb-2 %p.mb-2

View File

@@ -1,6 +1,11 @@
- if current_member && current_member != member # must be logged in, can't follow yourself - if current_member && current_member != member # must be logged in, can't follow yourself
- block = current_member.get_block(member)
- follow = current_member.get_follow(member) - follow = current_member.get_follow(member)
- if !follow && can?(:create, Follow) # not already following - if !block && !follow && can?(:create, Follow) # not already following, and not blocking
= link_to 'Follow', follows_path(followed: member), method: :post, class: 'btn btn-block btn-success' = link_to 'Follow', follows_path(followed: member), method: :post, class: 'btn btn-block btn-success'
- if follow && can?(:destroy, follow) # already following - if follow && can?(:destroy, follow) # already following
= link_to 'Unfollow', follow_path(follow), method: :delete, class: 'btn btn-block' = link_to 'Unfollow', follow_path(follow), method: :delete, class: 'btn btn-block'
- if !block && can?(:create, Block) # not already blocking
= link_to 'Block', blocks_path(blocked: member), method: :post, class: 'btn btn-block btn-danger'
- if block && can?(:destroy, block) # already blocking
= link_to 'Unblock', block_path(block), method: :delete, class: 'btn btn-block'

View File

@@ -0,0 +1,33 @@
%p Hello #{@member.login_name},
%h2= t('notifier_mailer.harvest_reminder.heading')
%p= t('notifier_mailer.harvest_reminder.intro')
%ul
- @plantings.each do |planting|
%li
= render 'planting', planting: planting
(Predicted harvest date: #{planting.first_harvest_predicted_at.to_date})
%p
Harvested anything lately?
= link_to "Track your harvests here.", new_harvest_url, class: 'btn'
%p
Track and predict your entire garden, and keep your garden records up to date at
= link_to member_gardens_url(@member), class: 'btn' do
your garden overview
and on
= link_to member_url(@member) do
your profile page
%h4
See you soon on #{@sitename}!
= render partial: 'signature'
%hr/
%p
Don't want to get these emails any more?
= link_to t('notifier_mailer.harvest_reminder.unsubscribe'), unsubscribe_member_url(message: @signed_message)

View File

@@ -21,13 +21,14 @@
Please select a photo from your recent uploads. Please select a photo from your recent uploads.
- if @sets && !@sets.empty? %p
%p = bootstrap_form_tag(url: new_photo_path, method: :get, layout: :inline) do |f|
= bootstrap_form_tag(url: new_photo_path, method: :get, layout: :inline) do |f| - if @sets && !@sets.empty?
= f.select :set, options_for_select(@sets, @current_set), label: "Choose a photo album" = f.select :set, options_for_select(@sets, @current_set), label: "Choose a photo album", include_blank: true
= hidden_field_tag :type, @type = f.text_field :tag, value: @current_tag, label: "or search by tag"
= hidden_field_tag :id, @id = hidden_field_tag :type, @type
= f.submit "Search", class: "btn btn-success" = hidden_field_tag :id, @id
= f.submit "Search", class: "btn btn-success"
- if @sets && @current_set - if @sets && @current_set
%h2= @sets.key(@current_set) %h2= @sets.key(@current_set)

View File

@@ -0,0 +1,26 @@
#modelFailedPlantingForm.modal.fade{"aria-hidden" => "true", "aria-labelledby" => "failed-planting-button", role: "dialog", tabindex: "-1"}
.modal-dialog{role: "document"}
.modal-content
.modal-header.text-center
%h4.modal-title.w-100.font-weight-bold Mark #{crop.name} planting as failed
%button.close{"aria-label" => "Close", "data-bs-dismiss" => "modal", type: "button"}
%span{"aria-hidden" => "true"} &#215;
.modal-body
%p Which planting would you like to mark as failed?
%ul.list-group
- active_plantings.each do |planting|
%li.list-group-item
= link_to planting_path(planting, planting: {failed: 1}), method: :put do
.d-flex.justify-content-between
%span
%h4= planting.garden.name
%p Planted #{planting.planted_at}
%span
= finished_icon
.mt-3.text-right
= link_to 'cancel', '', "data-bs-dismiss" => "modal", class: 'btn btn-secondary'
%a.btn#failed-planting-button{"data-bs-target" => "#modelFailedPlantingForm", "data-bs-toggle" => "modal", href: ""}
= finished_icon
Mark as failed

View File

@@ -4,7 +4,7 @@
- content_for :breadcrumbs do - content_for :breadcrumbs do
- if @owner - if @owner
%li.breadcrumb-item= link_to 'Plantings', plantings_path %li.breadcrumb-item= link_to 'Plantings', plantings_path
%li.breadcrumb-item.active= link_to "#{@owner}'s plantings", plantings_path(owner: @owner) %li.breadcrumb-item.active= link_to "#{@owner}'s plantings", member_plantings_path(@owner)
- else - else
%li.breadcrumb-item.active= link_to 'Plantings', plantings_path %li.breadcrumb-item.active= link_to 'Plantings', plantings_path

View File

@@ -11,14 +11,14 @@
%p %p
%span.help-block %span.help-block
For detailed crop wrangling guidelines, please consult the For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "http://wiki.growstuff.org/index.php/Crop_wrangling" = link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
on the Growstuff wiki. on the Growstuff wiki.
.form-group .form-group
= f.label :crop_id, class: 'control-label col-md-2' = f.label :crop_id, class: 'control-label col-md-2'
.col-md-8 .col-md-8
= collection_select(:scientific_name, :crop_id, Crop.all.order(:name), :id, = select(:scientific_name, :crop_id, Crop.order(:name).pluck(:name, :id),
:name, { selected: @scientific_name.crop_id || @crop.id }, { selected: @scientific_name.crop_id || @crop.id },
class: 'form-control') class: 'form-control')
.form-group .form-group
= f.label :name, class: 'control-label col-md-2' = f.label :name, class: 'control-label col-md-2'

View File

@@ -32,7 +32,7 @@ csv.headers :id,
csv.cell :quantity csv.cell :quantity
csv.cell :plant_before, s.plant_before ? s.plant_before.to_s(:db) : '' csv.cell :plant_before, s.plant_before ? s.plant_before.to_fs(:db) : ''
csv.cell :tradable_to csv.cell :tradable_to
csv.cell :from_location, s.owner.location csv.cell :from_location, s.owner.location

View File

@@ -1,7 +1,7 @@
- content_for :breadcrumbs do - content_for :breadcrumbs do
- if @owner - if @owner
%li.breadcrumb-item= link_to 'Seeds', seeds_path %li.breadcrumb-item= link_to 'Seeds', seeds_path
%li.breadcrumb-item.active= link_to "#{@owner}'s seeds", seeds_path(owner: @owner) %li.breadcrumb-item.active= link_to "#{@owner}'s seeds", member_seeds_path(@owner)
- else - else
%li.breadcrumb-item.active= link_to 'Seeds', seeds_path %li.breadcrumb-item.active= link_to 'Seeds', seeds_path

View File

@@ -3,4 +3,4 @@
%button.close{ type: "button", "data-bs-dismiss" => "alert" } %button.close{ type: "button", "data-bs-dismiss" => "alert" }
%span{ "aria-hidden" => true } &times; %span{ "aria-hidden" => true } &times;
%span.sr-only Close %span.sr-only Close
= content = sanitize(content)

View File

@@ -1,24 +0,0 @@
# frozen_string_literal: true
# Require any additional compass plugins here.
# Set this to the root of your project when deployed:
http_path = "/"
css_dir = "app/assets/stylesheets"
sass_dir = "app/assets/stylesheets"
javascripts_dir = "app/assets/javascripts"
images_dir = "app/assets/images"
# You can select your preferred output style here (can be overridden via the command line):
# output_style = :expanded or :nested or :compact or :compressed
# To enable relative paths to assets via compass helper functions. Uncomment:
# relative_assets = true
# To disable debugging comments that display the original location of your selectors. Uncomment:
# line_comments = false
# If you prefer the indented syntax, you might want to regenerate this
# project again passing --syntax sass, or you can uncomment this:
preferred_syntax = :sass
# and then run:
# sass-convert -R --from scss --to sass sass scss && rm -rf sass && mv scss sass

View File

@@ -73,6 +73,8 @@ module Growstuff
config.newsletter_list_id = ENV.fetch('GROWSTUFF_MAILCHIMP_NEWSLETTER_ID', nil) config.newsletter_list_id = ENV.fetch('GROWSTUFF_MAILCHIMP_NEWSLETTER_ID', nil)
# config.active_record.raise_in_transactional_callbacks = true # config.active_record.raise_in_transactional_callbacks = true
config.middleware.insert_before 0, Rack::Attack
config.middleware.insert_before 0, Rack::Cors do config.middleware.insert_before 0, Rack::Cors do
allow do allow do
origins '*' origins '*'

View File

@@ -1,4 +0,0 @@
# frozen_string_literal: true
# Require any additional compass plugins here.
project_type = :rails

View File

@@ -16,10 +16,6 @@ Rails.application.configure do
config.consider_all_requests_local = false config.consider_all_requests_local = false
config.action_controller.perform_caching = true config.action_controller.perform_caching = true
# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
# `config/secrets.yml.key`.
config.read_encrypted_secrets = true
# Disable serving static files from the `/public` folder by default since # Disable serving static files from the `/public` folder by default since
# Apache or NGINX already handles this. # Apache or NGINX already handles this.

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# Be sure to restart your server when you modify this file. # Be sure to restart your server when you modify this file.
# #
# This file contains migration options to ease your Rails 6.0 upgrade. # This file contains migration options to ease your Rails 6.0 upgrade.

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
class Rack::Attack
### Throttle Config ###
if Rails.env.production?
# Throttle requests to /plantings, /harvests, and /members to 10 per minute per IP
# Includes API routes
throttle('req/ip/restricted_routes', limit: 20, period: 1.minute) do |req|
if req.path =~ %r{^/(plantings|harvests|members)(/|$)} || req.path =~ %r{^/api/v1/(plantings|harvests|members)(/|$)}
req.ip
end
end
### Fail2Ban Config ###
# Block IPs that make too many requests to suspicious paths
# After 5 "bad" requests in 10 minutes, block the IP for 1 hour
blocklist('fail2ban/pentesters') do |req|
Fail2Ban.filter("pentesters-#{req.ip}", maxretry: 5, findtime: 10.minutes, bantime: 1.hour) do
# The count for the IP is incremented if the return value is truthy.
req.path.include?('wp-admin') ||
req.path.include?('wp-login') ||
req.path.include?('cgi-bin') ||
req.path.end_with?('.php', '.asp', '.aspx', '.jsp', '.exe', '.env', '.git')
end
end
end
### Custom Response Headers ###
# Add Retry-After header to throttled responses
self.throttled_response_retry_after_header = true
end

View File

@@ -3,9 +3,9 @@
# config/initializers/sidekiq.rb # config/initializers/sidekiq.rb
Sidekiq.configure_server do |config| Sidekiq.configure_server do |config|
config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" } config.redis = { url: 'redis://localhost:6379/0' }
end end
Sidekiq.configure_client do |config| Sidekiq.configure_client do |config|
config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" } config.redis = { url: 'redis://localhost:6379/0' }
end end

View File

@@ -63,6 +63,56 @@ en:
seed: seed:
one: seed one: seed
other: seeds other: seeds
errors:
messages:
crop_not_found: must be present and exist in our database
crop_must_be_approved: must be present and exist in our database
failed_and_finished: "can't be true if planting is also finished"
finished_after_planted: must be after the planting date
rejection_reason_required: must be rejected if a reason for rejection is present
rejection_notes_required: "must be added if the reason for rejection is \"other\""
same_crop_required: must be the same crop
harvest_after_planted: cannot be harvested before planting
cannot_be_garden_owner: cannot be the garden owner
same_owner_required: "of harvest must be the same as planting, or a collaborator on that garden"
photo_owner_mismatch: must have same owner as item it links to
calculate_crop_failed: failed to calculate crop
not_a_valid_wikipedia_url: is not a valid English Wikipedia URL
not_a_valid_youtube_url: is not a valid YouTube URL
not_a_valid_sunniness: "%{value} is not a valid sunniness value"
not_a_valid_planting_method: "%{value} is not a valid planting method"
not_a_valid_unit: "%{value} is not a valid unit"
not_a_valid_area_unit: "%{value} is not a valid area unit"
no_newlines: must contain no newlines
models:
member:
attributes:
login_name:
login_name_length: should be between 2 and 25 characters long
login_name_reserved: name is reserved
login_name_format: may only include letters, numbers, or underscores
website_url:
url_format: "must start with http:// or https://"
other_url:
url_format: "must start with http:// or https://"
instagram_handle:
handle_format: should be a handle, not a URL
facebook_handle:
handle_format: should be a handle, not a URL
bluesky_handle:
handle_format: should be a handle, not a URL
seed:
attributes:
tradable_to:
tradable_to_inclusion: "You may only trade seed nowhere, locally, nationally, or internationally"
organic:
organic_inclusion: "You must say whether the seeds are organic or not, or that you don't know"
gmo:
gmo_inclusion: "You must say whether the seeds are genetically modified or not, or that you don't know"
heirloom:
heirloom_inclusion: "You must say whether the seeds are heirloom, hybrid, or unknown"
source:
source_inclusion: "You must say where the seeds are from, or that you don't know"
application_helper: application_helper:
title: title:
title: title:
@@ -112,6 +162,9 @@ en:
forums: forums:
index: index:
title: Forums title: Forums
created: Forum was successfully created.
updated: Forum was successfully updated.
deleted: Forum was successfully deleted.
gardens: gardens:
created: Garden was successfully created. created: Garden was successfully created.
deleted: Garden was successfully deleted. deleted: Garden was successfully deleted.
@@ -208,6 +261,8 @@ en:
trade_to: Will trade to trade_to: Will trade to
unspecified: unspecified unspecified: unspecified
view_all: View all seeds view_all: View all seeds
added_to_stash: Successfully added %{crop} seed to your stash.
updated: Seed was successfully updated.
stats: stats:
member_linktext: "%{count} members" member_linktext: "%{count} members"
message_html: So far, %{member} have planted %{number_crops} %{number_plantings} in %{number_gardens}; and %{contributors} people have contributed to our code on %{github}! message_html: So far, %{member} have planted %{number_crops} %{number_plantings} in %{number_gardens}; and %{contributors} people have contributed to our code on %{github}!
@@ -275,6 +330,21 @@ en:
links: links:
my_gardens: My gardens my_gardens: My gardens
messages:
auth_success: Authentication successful.
auth_failed: Authentication failed.
crops_waiting: There are crops waiting to be wrangled.
followed: "Followed %{name}"
unfollowed: "Unfollowed %{name}"
follow_error: Already following or error while following.
transplant_success: Planting was successfully transplanted.
transplant_error: "There was an error transplanting the planting: %{errors}"
revert_success: "Reverted to version from %{date}"
revert_error: "Could not revert to version from %{date}. Errors: %{errors}"
invalid_location: Please enter a valid location
unable_to_like: Unable to like
unable_to_unlike: Unable to unlike
members: members:
edit_profile: Edit profile edit_profile: Edit profile
index: index:
@@ -342,10 +412,22 @@ en:
progress_0_not_planted_yet: 'Progress: 0% - not planted yet' progress_0_not_planted_yet: 'Progress: 0% - not planted yet'
posts: posts:
write_blog_post: Write blog post write_blog_post: Write blog post
created: Post was successfully created.
updated: Post was successfully updated.
deleted: Post was deleted.
index: index:
title: title:
author_posts: "%{author} posts" author_posts: "%{author} posts"
default: Everyone's posts default: Everyone's posts
scientific_names:
deleted: Scientific name was successfully deleted.
alternate_names:
created: Alternate name was successfully created.
updated: Alternate name was successfully updated.
deleted: Alternate name was successfully deleted.
crop_companions:
created: Companion was successfully created.
deleted: Companion was successfully destroyed.
seeds: seeds:
form: form:
trade_help: > trade_help: >
@@ -361,6 +443,7 @@ en:
owner_seeds: "%{owner} seeds" owner_seeds: "%{owner} seeds"
save_seeds: Save seeds save_seeds: Save seeds
string: "%{crop} seeds belonging to %{owner}" string: "%{crop} seeds belonging to %{owner}"
added_to_stash: Successfully added %{crop} seed to your stash.
unauthorized: unauthorized:
create: create:
all: Please sign in or sign up to create a %{subject}. all: Please sign in or sign up to create a %{subject}.
@@ -373,3 +456,9 @@ en:
all: Not authorized to %{action} %{subject}. all: Not authorized to %{action} %{subject}.
read: read:
notification: You must be signed in to view notifications. notification: You must be signed in to view notifications.
notifier_mailer:
harvest_reminder:
subject: "Upcoming harvests in your garden - %{sitename}"
heading: "Upcoming harvests in your garden"
intro: "Based on our predictions, the following plantings in your garden are likely to be ready for harvest within the next week:"
unsubscribe: "Unsubscribe from harvest reminders"

View File

@@ -32,6 +32,7 @@ Rails.application.routes.draw do
resources :gardens, concerns: :has_photos, param: :slug do resources :gardens, concerns: :has_photos, param: :slug do
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' } get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
post 'fetch_wikidata' => 'gardens#fetch_wikidata', on: :member
resources :garden_collaborators resources :garden_collaborators
end end
@@ -105,6 +106,7 @@ Rails.application.routes.draw do
resources :forums resources :forums
resources :follows, only: %i(create destroy) resources :follows, only: %i(create destroy)
resources :blocks, only: %i(create destroy)
post 'likes' => 'likes#create' post 'likes' => 'likes#create'
delete 'likes' => 'likes#destroy' delete 'likes' => 'likes#destroy'
@@ -121,6 +123,7 @@ Rails.application.routes.draw do
resources :follows resources :follows
get 'followers' => 'follows#followers' get 'followers' => 'follows#followers'
resources :blocks, only: %i(create destroy)
end end
resources :messages resources :messages
@@ -155,6 +158,7 @@ Rails.application.routes.draw do
namespace :api do namespace :api do
namespace :v1 do namespace :v1 do
jsonapi_resources :activities jsonapi_resources :activities
get "crops/search", to: "crops#search"
jsonapi_resources :crops jsonapi_resources :crops
jsonapi_resources :gardens jsonapi_resources :gardens
jsonapi_resources :harvests jsonapi_resources :harvests

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
# This migration comes from active_storage (originally 20180723000244) # This migration comes from active_storage (originally 20180723000244)
class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0] class AddForeignKeyConstraintToActiveStorageAttachmentsForBlobId < ActiveRecord::Migration[6.0]
def up def up

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddFieldsToCrops < ActiveRecord::Migration[5.2] class AddFieldsToCrops < ActiveRecord::Migration[5.2]
def change def change
add_column :crops, :row_spacing, :integer add_column :crops, :row_spacing, :integer

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class PopulateCropFieldsFromOpenfarmData < ActiveRecord::Migration[5.2] class PopulateCropFieldsFromOpenfarmData < ActiveRecord::Migration[5.2]
def up def up
Crop.find_each do |crop| Crop.find_each do |crop|

View File

@@ -15,7 +15,5 @@ class AddActivities < ActiveRecord::Migration[7.1]
end end
add_column :members, :activities_count, :integer add_column :members, :activities_count, :integer
Activity.reindex
end end
end end

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class AddSocialMediaToMembers < ActiveRecord::Migration[6.0] class AddSocialMediaToMembers < ActiveRecord::Migration[6.0]
def change def change
add_column :members, :website_url, :string add_column :members, :website_url, :string

Some files were not shown because too many files have changed in this diff Show More