Compare commits

..

123 Commits

Author SHA1 Message Date
Daniel O'Connor
7bb632c3c1 Merge branch 'dev' into feature/set-location 2026-05-03 12:56:28 +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
google-labs-jules[bot]
80e724fefe feat: Add set location feature with privacy enhancements
This commit introduces a new feature that allows members to set their location using two methods: a "Find my location" button that uses the browser's Geolocation API, and a Leaflet.js map for manual marker placement.

Key changes:
- Adds a new `set_location` page for members to update their location.
- Integrates a Leaflet.js map for visual location selection.
- Implements a "Find my location" button using the browser's Geolocation API.
- Uses reverse geocoding to determine a user's suburb/city/town from coordinates.
- Enhances privacy by rounding latitude and longitude to two decimal places, reducing location accuracy.
- Provides a fallback mechanism for when reverse geocoding fails, storing a generic location string.
- Adds comprehensive feature tests for the new functionality.
2025-09-01 10:16:20 +00:00
216 changed files with 2668 additions and 1031 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
# `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
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
# Offense count: 19
Capybara/NegationMatcherAfterVisit:
# Offense count: 407
# This cop supports safe autocorrection (--autocorrect).
Capybara/RSpec/HaveContent:
Enabled: false
# Offense count: 21
Capybara/RSpec/NegationMatcherAfterVisit:
Exclude:
- 'spec/features/admin/reverting_crops_spec.rb'
- 'spec/features/crops/crop_detail_page_spec.rb'
- 'spec/features/crops/crop_wranglers_spec.rb'
- 'spec/features/gardens/gardens_spec.rb'
- 'spec/features/members/blocking_spec.rb'
- 'spec/features/members/deletion_spec.rb'
- 'spec/features/members/following_spec.rb'
- 'spec/features/members/profile_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
# Offense count: 14
Capybara/SpecificMatcher:
Capybara/RSpec/SpecificMatcher:
Exclude:
- 'spec/features/footer_spec.rb'
- 'spec/features/gardens/adding_gardens_spec.rb'
@@ -28,7 +34,7 @@ Capybara/SpecificMatcher:
- 'spec/features/seeds/adding_seeds_spec.rb'
# Offense count: 1
Capybara/VisibilityMatcher:
Capybara/RSpec/VisibilityMatcher:
Exclude:
- 'spec/features/shared_examples/crop_suggest.rb'
@@ -63,7 +69,13 @@ FactoryBot/ExcessiveCreateList:
- 'spec/features/crops/show_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).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table
@@ -81,7 +93,7 @@ Layout/HashAlignment:
- 'spec/requests/api/v1/activities_request_spec.rb'
- 'spec/requests/api/v1/members_request_spec.rb'
# Offense count: 6
# Offense count: 8
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
@@ -89,10 +101,20 @@ Layout/LineLength:
Exclude:
- 'Gemfile'
- 'app/controllers/admin/versions_controller.rb'
- 'app/controllers/crops_controller.rb'
- 'app/models/concerns/predict_planting.rb'
- 'app/models/crop.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
# This cop supports safe autocorrection (--autocorrect).
@@ -100,23 +122,15 @@ Lint/AmbiguousOperatorPrecedence:
Exclude:
- 'app/controllers/activities_controller.rb'
# Offense count: 4
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: RequireParenthesesForMethodChains.
Lint/AmbiguousRange:
Exclude:
- 'app/models/concerns/search_activities.rb'
- 'app/models/concerns/search_harvests.rb'
- 'app/models/concerns/search_plantings.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
# Configuration parameters: AllowedMethods.
# AllowedMethods: enums
@@ -154,27 +168,12 @@ Lint/SuppressedException:
Exclude:
- '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
Lint/UselessConstantScoping:
Exclude:
- 'app/controllers/members_controller.rb'
# Offense count: 61
# Offense count: 65
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 295
@@ -196,12 +195,12 @@ Metrics/CollectionLiteralLength:
Exclude:
- 'lib/tasks/import.rake'
# Offense count: 10
# Offense count: 11
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 32
# Offense count: 82
# Offense count: 83
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Max: 296
@@ -211,7 +210,7 @@ Metrics/MethodLength:
Metrics/ModuleLength:
Max: 144
# Offense count: 8
# Offense count: 10
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 32
@@ -225,6 +224,16 @@ Naming/PredicateMethod:
- 'app/models/concerns/finishable.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
RSpec/AnyInstance:
Exclude:
@@ -237,89 +246,54 @@ RSpec/BeEq:
Exclude:
- 'spec/requests/api/v1/activities_request_spec.rb'
# Offense count: 1
# Offense count: 2
RSpec/BeforeAfterAll:
Exclude:
- 'spec/tasks/import_spec.rb'
- 'spec/tasks/members_spec.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
RSpec/ContextMethod:
Exclude:
- 'spec/requests/api/v1/activities_request_spec.rb'
# Offense count: 299
# Offense count: 311
# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
Enabled: false
# Offense count: 1
# Offense count: 2
# Configuration parameters: IgnoredMetadata.
RSpec/DescribeClass:
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).
# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
# SupportedStyles: described_class, explicit
RSpec/DescribedClass:
Exclude:
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
- 'spec/models/like_spec.rb'
- 'spec/models/member_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
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowConsecutiveOneLiners.
RSpec/EmptyLineAfterExample:
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.
RSpec/ExampleLength:
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
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
@@ -334,28 +308,38 @@ RSpec/HooksBeforeExamples:
Exclude:
- 'spec/features/crops/creating_a_crop_spec.rb'
# Offense count: 37
# Offense count: 53
# This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/IncludeExamples:
Exclude:
- 'spec/features/conversations/index_spec.rb'
- 'spec/features/crops/alternate_name_spec.rb'
- 'spec/features/crops/browse_crops_spec.rb'
- 'spec/features/crops/creating_a_crop_spec.rb'
- 'spec/features/crops/crop_photos_spec.rb'
- 'spec/features/crops/delete_crop_spec.rb'
- 'spec/features/gardens/actions_spec.rb'
- 'spec/features/gardens/adding_gardens_spec.rb'
- 'spec/features/gardens/index_spec.rb'
- 'spec/features/likeable_spec.rb'
- 'spec/features/signout_spec.rb'
- 'spec/models/crop_spec.rb'
- 'spec/support/feature_helpers.rb'
- 'spec/views/photos/show.html.haml_spec.rb'
- 'spec/views/seeds/index.rss.haml_spec.rb'
# Offense count: 2
# Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns.
RSpec/IndexedLet:
Exclude:
- 'spec/controllers/harvests_controller_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'
- 'spec/models/activity_spec.rb'
# Offense count: 719
# Offense count: 711
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Enabled: false
# Offense count: 41
# Offense count: 43
RSpec/LetSetup:
Enabled: false
@@ -370,7 +354,7 @@ RSpec/MessageChain:
Exclude:
- 'spec/models/member_spec.rb'
# Offense count: 23
# Offense count: 65
# Configuration parameters: .
# SupportedStyles: have_received, receive
RSpec/MessageSpies:
@@ -381,11 +365,11 @@ RSpec/MultipleDescribes:
Exclude:
- 'spec/features/crops/crop_wranglers_spec.rb'
# Offense count: 189
# Offense count: 235
RSpec/MultipleExpectations:
Max: 19
# Offense count: 166
# Offense count: 171
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 16
@@ -396,24 +380,35 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NamedSubject:
Enabled: false
# Offense count: 111
# Offense count: 112
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 6
# Offense count: 407
# Offense count: 366
# Configuration parameters: AllowedPatterns.
# AllowedPatterns: ^expect_, ^assert_
RSpec/NoExpectationExample:
Enabled: false
# Offense count: 4
# Offense count: 9
RSpec/PendingWithoutReason:
Exclude:
- 'spec/features/members/blocking_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/features/seeds/misc_seeds_spec.rb'
- 'spec/features/unsubscribing_spec.rb'
- 'spec/models/ability_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
RSpec/RepeatedDescription:
Exclude:
@@ -438,18 +433,18 @@ RSpec/ScatteredSetup:
- 'spec/features/percy/percy_spec.rb'
- 'spec/features/plantings/prediction_spec.rb'
# Offense count: 1
# Offense count: 2
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
# SupportedInflectors: default, active_support
RSpec/SpecFilePathFormat:
Exclude:
- 'spec/controllers/member_controller_spec.rb'
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
# Offense count: 3
# Offense count: 2
RSpec/StubbedMock:
Exclude:
- 'spec/controllers/garden_types_controller_spec.rb'
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/controllers/photos_controller_spec.rb'
- 'spec/models/member_spec.rb'
# Offense count: 1
@@ -466,22 +461,12 @@ RSpec/VerifiedDoubles:
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/views/devise/shared/_links_spec.rb'
# Offense count: 7
# 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
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Inferences.
RSpecRails/InferredSpecType:
Enabled: false
Exclude:
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
# Offense count: 30
# Configuration parameters: Database.
@@ -539,14 +524,24 @@ Rails/HasManyOrHasOneDependent:
- 'app/models/crop.rb'
- 'app/models/member.rb'
# Offense count: 7
Rails/HelperInstanceVariable:
Exclude:
- 'app/helpers/crops_helper.rb'
- 'app/helpers/plantings_helper.rb'
# Offense count: 1
Rails/I18nLocaleAssignment:
Exclude:
- 'spec/features/locale_spec.rb'
# Offense count: 40
# Offense count: 5
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
# Configuration parameters: IgnoreScopes.
@@ -561,6 +556,12 @@ Rails/LexicallyScopedActionFilter:
- 'app/controllers/data_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
Rails/OutputSafety:
Exclude:
@@ -589,7 +590,7 @@ Rails/RakeEnvironment:
- 'lib/tasks/i18n.rake'
- 'lib/tasks/testing.rake'
# Offense count: 9
# Offense count: 8
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedReceivers.
# AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone
@@ -602,7 +603,6 @@ Rails/RedundantActiveRecordAllMethod:
- 'app/controllers/scientific_names_controller.rb'
- 'spec/features/members/deletion_spec.rb'
- 'spec/features/percy/percy_spec.rb'
- 'spec/models/harvest_spec.rb'
# Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -644,15 +644,6 @@ Rails/RootPathnameMethods:
- 'lib/tasks/import.rake'
- '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
Rails/ThreeStateBooleanColumn:
Enabled: false
@@ -684,12 +675,6 @@ Rails/WhereEquals:
- 'app/models/harvest.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
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/WhereRange:
@@ -703,7 +688,7 @@ Rake/MethodDefinitionInTask:
Exclude:
- 'lib/tasks/growstuff.rake'
# Offense count: 4
# Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
# SupportedStyles: nested, compact
@@ -712,10 +697,17 @@ Rake/MethodDefinitionInTask:
Style/ClassAndModuleChildren:
Exclude:
- 'app/controllers/admin/crops_controller.rb'
- 'config/initializers/rack_attack.rb'
- 'lib/actions/oauth_signup_action.rb'
- 'lib/haml/filters/escaped_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
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/CommentedKeyword:
@@ -746,12 +738,14 @@ Style/FloatDivision:
Exclude:
- 'app/models/concerns/predict_planting.rb'
# Offense count: 22
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
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
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -766,11 +760,14 @@ Style/IdenticalConditionalBranches:
Exclude:
- 'lib/actions/oauth_signup_action.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/MapIntoArray:
# Offense count: 4
# This cop supports safe autocorrection (--autocorrect).
Style/IfUnlessModifier:
Exclude:
- 'app/helpers/crops_helper.rb'
- 'app/models/concerns/predict_planting.rb'
- 'config/initializers/rack_attack.rb'
- 'lib/tasks/growstuff.rake'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -817,13 +814,6 @@ Style/OptionalBooleanParameter:
- 'app/helpers/application_helper.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
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Methods.
@@ -831,6 +821,13 @@ Style/RedundantArgument:
Exclude:
- '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
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SafeForConstants.

View File

@@ -1 +1 @@
3.4.8
3.4.9

View File

@@ -248,6 +248,3 @@ linters:
ZeroUnit:
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 'rack-attack'
gem 'loofah', '>= 2.19.1'
gem 'rack-protection', '>= 2.0.1'

View File

@@ -140,15 +140,15 @@ GEM
aws-sigv4 (~> 1.5)
aws-sigv4 (1.12.1)
aws-eventstream (~> 1, >= 1.0.2)
axe-core-api (4.11.2)
axe-core-api (4.11.3)
dumb_delegator
ostruct
virtus
axe-core-capybara (4.11.2)
axe-core-api (= 4.11.2)
axe-core-capybara (4.11.3)
axe-core-api (= 4.11.3)
dumb_delegator
axe-core-rspec (4.11.2)
axe-core-api (= 4.11.2)
axe-core-rspec (4.11.3)
axe-core-api (= 4.11.3)
dumb_delegator
ostruct
virtus
@@ -276,7 +276,7 @@ GEM
elasticsearch-transport (7.0.0)
faraday
multi_json
erb (6.0.2)
erb (6.0.4)
erubi (1.13.1)
execjs (2.10.0)
factory_bot (6.5.5)
@@ -376,7 +376,7 @@ GEM
rails-dom-testing (>= 1, < 3)
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.19.3)
json (2.19.4)
json-schema (6.2.0)
addressable (~> 2.8)
bigdecimal (>= 3.1, < 5)
@@ -477,7 +477,7 @@ GEM
paper_trail (17.0.0)
activerecord (>= 7.1)
request_store (~> 1.4)
parallel (2.0.1)
parallel (2.1.0)
parser (3.3.11.1)
ast (~> 2.4.1)
racc
@@ -498,11 +498,13 @@ GEM
date
stringio
public_suffix (7.0.5)
puma (8.0.0)
puma (8.0.1)
nio4r (~> 2.0)
query_diet (0.7.3)
racc (1.8.1)
rack (2.2.23)
rack-attack (6.8.0)
rack (>= 1.0, < 4)
rack-cors (2.0.2)
rack (>= 2.0.0)
rack-protection (3.2.0)
@@ -642,9 +644,9 @@ GEM
rubocop-ast (1.49.1)
parser (>= 3.3.7.2)
prism (~> 1.7)
rubocop-capybara (2.22.1)
rubocop-capybara (2.23.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop (~> 1.81)
rubocop-factory_bot (2.28.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
@@ -841,6 +843,7 @@ DEPENDENCIES
pry
puma
query_diet
rack-attack
rack-cors
rack-protection (>= 2.0.1)
rails (~> 7.2.0)
@@ -883,7 +886,7 @@ DEPENDENCIES
xmlrpc
RUBY VERSION
ruby 3.4.8p72
ruby 3.4.9
BUNDLED WITH
2.4.22

View File

@@ -30,3 +30,44 @@ if (document.getElementById("membermap") !== null) {
});
}
$(document).on('click', '#find-me', function(e) {
e.preventDefault();
if (navigator.geolocation) {
navigator.geolocation.getCurrentPosition(function(position) {
$('#member_latitude').val(position.coords.latitude);
$('#member_longitude').val(position.coords.longitude);
updateMap(position.coords.latitude, position.coords.longitude);
});
} else {
alert("Geolocation is not supported by this browser.");
}
});
if (document.getElementById("map") !== null) {
var map = L.map('map').setView([0, 0], 2);
var marker;
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors',
maxZoom: 18
}).addTo(map);
map.on('click', function(e) {
updateMarker(e.latlng);
$('#member_latitude').val(e.latlng.lat);
$('#member_longitude').val(e.latlng.lng);
});
function updateMarker(latlng) {
if (marker) {
map.removeLayer(marker);
}
marker = L.marker(latlng).addTo(map);
}
function updateMap(lat, lng) {
map.setView([lat, lng], 13);
updateMarker(L.latLng(lat, lng));
}
}

View File

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

View File

@@ -15,7 +15,7 @@ module Admin
def create
@crop_companion = @crop.crop_companions.new(crop_companion_params)
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
render :new
end
@@ -24,7 +24,7 @@ module Admin
def destroy
@crop_companion = @crop.crop_companions.find(params[:id])
@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
private

View File

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

View File

@@ -9,9 +9,9 @@ module Admin
@version = PaperTrail::Version.find(params[:id])
@object = @version.reify
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
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

View File

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

View File

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

View File

@@ -24,9 +24,9 @@ class AuthenticationsController < ApplicationController
name:
)
flash[:notice] = "Authentication successful."
flash[:notice] = t('messages.auth_success')
else
flash[:notice] = "Authentication failed."
flash[:notice] = t('messages.auth_failed')
end
redirect_to request.env['omniauth.origin'] || edit_member_registration_path
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
class CropsController < ApplicationController
respond_to :json
before_action :set_crop
def sunniness
pie_chart_query 'sunniness'
@@ -13,20 +14,28 @@ module Charts
end
def harvested_for
@crop = Crop.find_by!(slug: params[:crop_slug])
render json: Harvest.joins(:plant_part)
.where(crop: @crop)
.group("plant_parts.name").count(:id)
data = Rails.cache.fetch("#{@crop.cache_key_with_version}/harvested_for", expires_in: 1.day) do
Harvest.joins(:plant_part)
.where(crop: @crop)
.group("plant_parts.name").count(:id)
end
render json: data
end
private
def pie_chart_query(field)
def set_crop
@crop = Crop.find_by!(slug: params[:crop_slug])
render json: Planting.where(crop: @crop)
.where.not(field.to_sym => nil)
.where.not(field.to_sym => '')
.group(field.to_sym).count(:id)
end
def pie_chart_query(field)
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

View File

@@ -13,7 +13,7 @@ class CropsController < ApplicationController
@crops = Crop.search('*', boost_by: %i(plantings_count harvests_count),
limit: 100,
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
@filename = filename
respond_with @crops
@@ -160,7 +160,7 @@ class CropsController < ApplicationController
when 'youtube'
Crop.approved.where(en_youtube_url: [nil, '']).order(plantings_count: :desc)
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'
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)

View File

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

View File

@@ -32,14 +32,14 @@ class ForumsController < ApplicationController
# POST /forums.json
def create
@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)
end
# PUT /forums/1
# PUT /forums/1.json
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)
end
@@ -47,7 +47,7 @@ class ForumsController < ApplicationController
# DELETE /forums/1.json
def destroy
@forum.destroy
flash[:notice] = 'Forum was successfully deleted'
flash[:notice] = t('forums.deleted')
redirect_to forums_url
end

View File

@@ -2,7 +2,7 @@
class GardensController < DataController
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_jump_to = params[:member_slug].present? || false

View File

@@ -5,8 +5,8 @@ class HarvestsController < DataController
def index
where = {}
if params[:member_slug]
@owner = Member.find_by(slug: params[:member_slug])
if params[:member_slug].present?
@owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id
end
@@ -23,7 +23,7 @@ class HarvestsController < DataController
@harvests = Harvest.search('*', where:,
limit: 100,
page: params[:page],
load: false,
load: (request.format.csv? ? { include: %i(crop owner plant_part) } : false),
boost_by: [:created_at])
@filename = csv_filename

View File

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

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MembersController < ApplicationController
load_and_authorize_resource except: %i(finish_signup unsubscribe view_follows view_followers show)
load_and_authorize_resource except: %i(finish_signup unsubscribe view_follows view_followers show set_location update_location)
skip_authorize_resource only: %i(nearby unsubscribe finish_signup)
respond_to :html, :json, :rss
@@ -86,15 +86,46 @@ class MembersController < ApplicationController
end
end
def set_location
@member = Member.find_by_slug!(params[:id])
authorize! :update, @member
end
def update_location
@member = Member.find_by_slug!(params[:id])
authorize! :update, @member
if params[:member][:latitude].present? && params[:member][:longitude].present?
lat = params[:member][:latitude].to_f.round(2)
lng = params[:member][:longitude].to_f.round(2)
params[:member][:latitude] = lat
params[:member][:longitude] = lng
results = Geocoder.search([lat, lng])
if results.first
params[:member][:location] = results.first.city || results.first.town || results.first.village || results.first.hamlet
else
params[:member][:location] = "Location near #{lat}, #{lng}"
end
end
if @member.update(member_params)
redirect_to member_path(@member), notice: 'Location updated.'
else
render :set_location
end
end
private
EMAIL_TYPE_STRING = {
send_notification_email: "direct message notifications",
send_planting_reminder: "planting reminders"
send_planting_reminder: "planting reminders",
send_harvest_reminder: "harvest reminders"
}.freeze
def member_params
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter)
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :location, :latitude, :longitude, :send_harvest_reminder)
end
def member_json_fields

View File

@@ -27,10 +27,21 @@ class MessagesController < ApplicationController
def create
if params[:conversation_id].present?
@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])
redirect_to conversation_path(@conversation)
else
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]
subject = params[: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
def failure
flash[:alert] = "Authentication failed."
flash[:alert] = t('messages.auth_failed')
redirect_to request.env['omniauth.origin'] || "/"
end

View File

@@ -102,11 +102,12 @@ class PhotosController < ApplicationController
end
@current_set = params[:set]
@current_tag = params[:tag]
page = params[:page] || 1
@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|
pager.replace photos
@@ -118,6 +119,8 @@ class PhotosController < ApplicationController
{ crops: @crop.id }
elsif params[:planting_id]
{ planting_id: @planting.id }
elsif params[:planting_slug]
{ plantings: @planting.id }
else
{}
end
@@ -126,5 +129,6 @@ class PhotosController < ApplicationController
def set_crop_and_planting
@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_slug] if params[:planting_slug]
end
end

View File

@@ -33,7 +33,7 @@ class PlacesController < ApplicationController
def search
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
redirect_to place_path(params[:new_place])
end

View File

@@ -11,9 +11,9 @@ class PlantingsController < DataController
where = {}
where['active'] = true unless @show_all
if params[:member_slug]
@owner = Member.find_by(slug: params[:member_slug])
where['owner_id'] = @owner.id unless @owner.nil?
if params[:member_slug].present?
@owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id
end
if params[:crop_slug]
@@ -116,11 +116,11 @@ class PlantingsController < DataController
new_planting.finished_at = nil
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
# if the save fails, we should probably roll back the finishing of the original planting
@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
@@ -160,7 +160,7 @@ class PlantingsController < DataController
end
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('(saved_at IS NULL OR saved_at <= ?)', @planting.planted_at)
end

View File

@@ -8,7 +8,7 @@ class PostsController < ApplicationController
respond_to :rss, only: %i(index show)
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
respond_with(@posts)
end
@@ -21,6 +21,10 @@ class PostsController < ApplicationController
def new
@post = Post.new
@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)
end
@@ -29,17 +33,17 @@ class PostsController < ApplicationController
def create
params[:post][:author_id] = current_member.id
@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)
end
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)
end
def destroy
flash[:notice] = 'Post was deleted.' if @post.destroy
flash[:notice] = t('posts.deleted') if @post.destroy
respond_with(@post)
end

View File

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

View File

@@ -5,7 +5,7 @@ class SeedsController < DataController
where = {}
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
end
@@ -30,7 +30,7 @@ class SeedsController < DataController
page: params[:page],
limit: 30,
boost_by: [:created_at],
load: false
load: (request.format.csv? ? { include: %i(crop owner) } : false)
)
respond_with(@seeds)
@@ -61,7 +61,7 @@ class SeedsController < DataController
@seed.finished ||= false
@seed.owner = current_member
@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'
respond_with(@seed, location: @seed.parent_planting)
else
@@ -70,7 +70,7 @@ class SeedsController < DataController
end
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)
end

View File

@@ -5,7 +5,7 @@ class SessionsController < Devise::SessionsController
def create
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

View File

@@ -2,28 +2,47 @@
module CropsHelper
def crop_or_parent(crop, attribute)
default = crop.send(attribute)
return default if default.present?
@crop_or_parent_cache ||= {}
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
while parent = parent.parent
return parent.send(attribute) if parent&.send(attribute).present?
@crop_or_parent_cache[cache_key] = begin
value = crop.send(attribute)
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
# For scopes, arrays, etc return the empty value
default
end
def display_seed_availability(member, crop)
seeds = member.seeds.where(crop:)
total_quantity = seeds.where.not(quantity: nil).sum(:quantity)
@seed_availability_cache ||= {}
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
"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."
if seeds.none?
"You don't have any seeds of this crop."
else
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
@@ -40,53 +59,57 @@ module CropsHelper
end
def crop_jsonld_data(crop, full_attributes: true)
same_as_urls = [crop.en_wikipedia_url]
crop.scientific_names.each do |scientific_name|
same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}" if scientific_name.wikidata_id.present?
end
subject_of_entities = []
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
}
Rails.cache.fetch([crop.cache_key_with_version, "jsonld", full_attributes]) do
same_as_urls = [crop.en_wikipedia_url]
crop.scientific_names.each do |scientific_name|
if scientific_name.wikidata_id.present?
same_as_urls << "https://www.wikidata.org/wiki/#{scientific_name.wikidata_id}"
end
end
subject_of_entities = []
images = []
crop.photos.each do |photo|
images << photo.fullsize_url
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
crop.photos.each do |photo|
images << photo.fullsize_url
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
# 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

View File

@@ -46,9 +46,13 @@ module PlantingsHelper
# Returns a list of gardens the planting can be transplanted to
# based on the planting's owner.
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
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
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)
@member = member
@request = request

View File

@@ -164,6 +164,12 @@ class Ability
can :destroy, Follow
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 :update, GardenType
cannot :destroy, GardenType

View File

@@ -4,7 +4,6 @@ class Activity < ApplicationRecord
extend FriendlyId
include Ownable
include Finishable
include SearchActivities
include Likeable
belongs_to :garden, optional: true
@@ -46,4 +45,17 @@ class Activity < ApplicationRecord
def planting_slug
planting&.crop&.slug
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

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 :commentable, polymorphic: true, counter_cache: true
# validates :body, presence: true
validate :author_is_not_blocked
scope :post_order, -> { order(created_at: :asc) } # for display on post page
@@ -25,4 +26,14 @@ class Comment < ApplicationRecord
def to_s
"#{author.login_name} commented on #{commentable.subject}"
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

View File

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

View File

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

View File

@@ -13,40 +13,49 @@ module PredictPlanting
# dates
def finish_predicted_at
if planted_at.blank? || failed?
nil
elsif crop.median_lifespan.present?
planted_at + crop.median_lifespan.days
elsif crop.parent.present? && crop.parent.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days
end
return @finish_predicted_at if defined?(@finish_predicted_at)
@finish_predicted_at = if planted_at.blank? || failed?
nil
elsif crop.median_lifespan.present?
planted_at + crop.median_lifespan.days
elsif crop.parent.present? && crop.parent.median_lifespan.present?
planted_at + crop.parent.median_lifespan.days
end
end
# days
def expected_lifespan
if actual_lifespan.present?
actual_lifespan
elsif crop.median_lifespan.present?
crop.median_lifespan
elsif crop.parent.present? && crop.parent.median_lifespan.present?
crop.parent.median_lifespan
end
return @expected_lifespan if defined?(@expected_lifespan)
@expected_lifespan = if actual_lifespan.present?
actual_lifespan
elsif crop.median_lifespan.present?
crop.median_lifespan
elsif crop.parent.present? && crop.parent.median_lifespan.present?
crop.parent.median_lifespan
end
end
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
def age_in_days
return if planted_at.blank?
return if failed?
return @age_in_days if defined?(@age_in_days)
known_last_day ||= finished_at || Time.zone.today
known_last_day = Time.zone.today if known_last_day > Time.zone.today
@age_in_days = if planted_at.blank? || failed?
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
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,
format: {
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?
validates :en_youtube_url,
format: {
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
validates :name, uniqueness: { scope: :approval_status }, if: :pending?
@@ -190,12 +190,12 @@ class Crop < ApplicationRecord
return if rejected?
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
def must_have_meaningful_reason_for_rejection
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

View File

@@ -4,6 +4,7 @@ class Follow < ApplicationRecord
belongs_to :follower, class_name: "Member", inverse_of: :follows
belongs_to :followed, class_name: "Member", inverse_of: :inverse_follows
validates :follower_id, uniqueness: { scope: :followed_id }
validate :follower_is_not_blocked
after_create do
Notification.create(
@@ -14,4 +15,14 @@ class Follow < ApplicationRecord
notifiable: self
)
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

View File

@@ -32,7 +32,7 @@ class Garden < ApplicationRecord
validates :name, uniqueness: { scope: :owner_id }
validates :name,
format: { without: /\n/, message: "must contain no newlines" },
format: { without: /\n/, message: :no_newlines },
allow_blank: false, presence: true,
length: { maximum: 255 }
@@ -53,7 +53,7 @@ class Garden < ApplicationRecord
"acres" => "acre"
}.freeze
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
def cleanup_area

View File

@@ -11,7 +11,7 @@ class GardenCollaborator < ApplicationRecord
return unless member
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
def member_slug

View File

@@ -58,18 +58,18 @@ class Harvest < ApplicationRecord
##
## Validations
validates :crop, approved: true
validates :crop, presence: { message: "must be present and exist in our database" }
validates :plant_part, presence: { message: "must be present and exist in our database" }
validates :crop, presence: { message: :crop_not_found }
validates :plant_part, presence: { message: :crop_not_found }
validates :harvested_at, presence: true
validates :quantity, allow_nil: true, numericality: {
only_integer: false, greater_than_or_equal_to: 0
}
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_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 :owner_must_match_planting
@@ -109,37 +109,49 @@ class Harvest < ApplicationRecord
def to_s
# 50 individual apples, weighing 3lb
# 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
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
def unit_to_human
return "" unless quantity && unit
return 'individual' if unit == 'individual'
return "#{unit} of" if quantity == 1
"#{unit.pluralize} of"
@unit_to_human ||= begin
if !quantity || !unit
""
elsif unit == 'individual'
'individual'
elsif quantity == 1
"#{unit} of"
else
"#{unit.pluralize} of"
end
end
end
def weight_to_human
return "" unless weight_quantity
"weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}"
@weight_to_human ||= if weight_quantity
"weighing #{number_to_human(weight_quantity, strip_insignificant_zeros: true)} #{weight_unit}"
else
""
end
end
def crop_name_to_human
if unit != 'individual' # buckets of apricot*s*
crop.name.pluralize
elsif quantity == 1
crop.name
else
crop.name.pluralize
end.to_s
@crop_name_to_human ||= begin
if unit != 'individual' # buckets of apricot*s*
crop.name.pluralize
elsif quantity == 1
crop.name
else
crop.name.pluralize
end.to_s
end
end
private
@@ -147,7 +159,7 @@ class Harvest < ApplicationRecord
def crop_must_match_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
def owner_must_match_planting
@@ -155,14 +167,13 @@ class Harvest < ApplicationRecord
return if owner == planting.owner || planting.garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner,
"of harvest must be the same as planting, or a collaborator on that garden")
errors.add(:owner, :same_owner_required)
end
def harvest_must_be_after_planting
# only check if we are linked to a planting
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

View File

@@ -5,4 +5,24 @@ class Like < ApplicationRecord
belongs_to :likeable, polymorphic: true, counter_cache: true, touch: true
validates :member, :likeable, presence: true
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

View File

@@ -52,6 +52,15 @@ class Member < ApplicationRecord
has_many :followed, through: :follows
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
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 :has_plantings, -> { joins(:plantings).group("members.id") }
scope :wants_reminders, -> { where(send_planting_reminder: true) }
scope :wants_harvest_reminders, -> { where(send_harvest_reminder: true) }
# Include default devise modules. Others available are:
# :token_authenticatable, :confirmable,
@@ -96,21 +106,21 @@ class Member < ApplicationRecord
validates :tos_agreement, acceptance: { allow_nil: true, accept: true }
validates :login_name,
length: {
minimum: 2, maximum: 25, message: "should be between 2 and 25 characters long"
minimum: 2, maximum: 25, message: :login_name_length
},
exclusion: {
in: %w(growstuff admin moderator staff nearby), message: "name is reserved"
in: %w(growstuff admin moderator staff nearby), message: :login_name_reserved
},
format: {
with: /\A\w+\z/, message: "may only include letters, numbers, or underscores"
with: /\A\w+\z/, message: :login_name_format
},
uniqueness: {
case_sensitive: false
}
validates :website_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }, allow_blank: true
validates :other_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: :url_format }, allow_blank: true
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
@@ -152,7 +162,7 @@ class Member < ApplicationRecord
end
def unread_count
receipts.where(is_read: false).count
@unread_count ||= receipts.where(is_read: false).count
end
def self.login_name_or_email(login)
@@ -164,12 +174,12 @@ class Member < ApplicationRecord
end
def self.nearest_to(place)
nearby_members = []
if place
latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 })
nearby_members = Member.located.sort_by { |x| x.distance_from([latitude, longitude]) } if latitude && longitude
end
nearby_members
return [] if place.blank?
latitude, longitude = Geocoder.coordinates(place, params: { limit: 1 })
return [] unless latitude && longitude
Member.located.near([latitude, longitude], 1000)
end
def already_following?(member)
@@ -179,4 +189,33 @@ class Member < ApplicationRecord
def get_follow(member)
follows.find_by(followed_id: member.id) if already_following?(member)
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

View File

@@ -29,12 +29,12 @@ class PhotoAssociation < ApplicationRecord
def photo_and_item_have_same_owner
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
def crop_present
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

View File

@@ -72,7 +72,7 @@ class Planting < ApplicationRecord
##
## Validations
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 :owner_must_match_garden_owner
validate :cannot_be_finished_and_failed
@@ -80,10 +80,10 @@ class Planting < ApplicationRecord
only_integer: true, greater_than_or_equal_to: 0
}
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: {
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: {
only_integer: true, greater_than_or_equal_to: 1, less_than_or_equal_to: 5
@@ -119,33 +119,36 @@ class Planting < ApplicationRecord
end
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 })
Planting.joins(:garden)
.where(crop:)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
@nearby_same_crop = if location.blank? || latitude.blank? || longitude.blank?
Planting.none
else
# latitude, longitude = Geocoder.coordinates(location, params: { limit: 1 })
Planting.joins(:garden)
.where(crop:)
.located
.where('gardens.latitude < ? AND gardens.latitude > ?',
latitude + 10, latitude - 10)
end
end
private
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
# check that any finished_at date occurs after planted_at
def finished_must_be_after_planted
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
def owner_must_match_garden_owner
return if owner == garden.owner || garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner,
"must be the same as garden, or a collaborator on that garden")
errors.add(:owner, :same_owner_required)
end
end

View File

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

View File

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

View File

@@ -85,7 +85,7 @@ class GbifService
end
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}" }
update_crop(crop) if crop.valid?
rescue ActiveRecord::RecordInvalid

View File

@@ -3,7 +3,7 @@
- content_for :breadcrumbs do
- if @owner
%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
%li.breadcrumb-item.active= link_to 'Activities', activities_path

View File

@@ -16,14 +16,14 @@
%p
%span.help-block
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.
.form-group
= f.label :crop_id, class: 'control-label col-md-2'
.col-md-8
= collection_select(:alternate_name, :crop_id,
Crop.all, :id, :name,
= select(:alternate_name, :crop_id,
Crop.order(:name).pluck(:name, :id),
{ selected: @alternate_name.crop_id || @crop.id },
class: 'form-control')

View File

@@ -1,6 +1,12 @@
- if crop.approved? && signed_in?
- active_plantings = current_member.plantings.where(crop: crop).active
.btn-group.crop-actions{"aria-label" => "Crop Actions", role: "group"}
= render 'plantings/modal', planting: Planting.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)
= 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)
- 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'})
%span.help-block Optional. For setting up crop hierarchies for varieties etc.

View File

@@ -5,14 +5,15 @@
%p Nobody has harvested this crop yet.
- unless crop.harvests.empty?
%ul.list-group.list-group-flush
- Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest|
%li.list-group-item
= link_to harvest_path(harvest), class: 'card-link' do
= harvest_icon
#{harvest.owner} harvested #{display_quantity(harvest)}.
.float-right= render 'members/location', member: harvest.owner
.harvest-timeago
%small #{standard_time_distance(harvest.harvested_at, Time.zone.now.to_date)}
- Rails.cache.fetch([crop, "recent_harvests", Time.zone.today]) do
- Harvest.where(crop: crop).includes(:owner).order(harvested_at: :desc).limit(3).each do |harvest|
%li.list-group-item
= link_to harvest_path(harvest), class: 'card-link' do
= harvest_icon
#{harvest.owner} harvested #{display_quantity(harvest)}.
.float-right= render 'members/location', member: harvest.owner
.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'
- if crop.approved?
- if current_member

View File

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

View File

@@ -6,7 +6,7 @@
Nobody has posted about #{crop.name.pluralize} yet.
%p
- 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
= render partial: "shared/signin_signup",
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|
csv.row c do |csv, crop|
csv.cells :id, :name, :en_wikipedia_url
csv.cell :growstuff_url, crop_url(c)
csv.cell :id, c.id
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?
csv.cell :default_scientific_name, c.default_scientific_name
@@ -58,10 +60,10 @@ csv.headers *all_headers
end
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
# Sunniness
# Sunniness
sunniness = c.sunniness
sunniness_rec = sunniness.max_by{|k,v| v}
@@ -74,7 +76,7 @@ csv.headers *all_headers
# Planted from
planted_from = c.planted_from
planted_from = c.planted_from
planted_from_rec = planted_from.max_by{|k,v| v}
if planted_from_rec
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
end
csv.cell :added_by_member_id, c.creator.id
csv.cell :added_by_member_name, c.creator.to_s
csv.cell :added_by_member_id, c.creator&.id
csv.cell :added_by_member_name, c.creator&.to_s
csv.cell :date_added, c.created_at.to_fs(:db)
csv.cell :last_modified, c.updated_at.to_fs(:db)
csv.cell :license, "CC-BY-SA Growstuff http://growstuff.org/"
end
end

View File

@@ -73,10 +73,11 @@
= pie_chart crop_harvested_for_path(@crop, format: :json), legend: "bottom"
- if @crop.varieties.any?
%section.varieties
%h2 Varieties
.index-cards
= render 'varieties', crop: @crop
- cache [@crop, 'varieties'] do
%section.varieties
%h2 Varieties
.index-cards
= render 'varieties', crop: @crop
%section.crop-map
%h2
@@ -134,9 +135,11 @@
= render 'harvests', 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
.card

View File

@@ -28,6 +28,12 @@
= f.check_box :send_planting_reminder
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
.col-md-offset-2.col-md-8.checkbox
%label

View File

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

View File

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

View File

@@ -4,7 +4,7 @@
- content_for :breadcrumbs do
- if @owner
%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
%li.breadcrumb-item.active= link_to "Harvests", harvests_path
.row

View File

@@ -1,6 +1,11 @@
- 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)
- 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'
- 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,23 @@
- content_for :title, "Set your location"
%h1 Set your location
= form_for @member, url: update_location_member_path(@member), method: :put do |f|
.form-group
= f.label :location
= f.text_field :location, class: 'form-control'
.form-group
= f.label :latitude
= f.text_field :latitude, class: 'form-control', readonly: true
.form-group
= f.label :longitude
= f.text_field :longitude, class: 'form-control', readonly: true
#map.set-location-map
.form-group
%button#find-me.btn.btn-default Find my location
= f.submit 'Update location', class: 'btn btn-primary'

View File

@@ -65,6 +65,9 @@
= link_to edit_member_registration_path, class: 'btn btn-block' do
= member_icon
= t('members.edit_profile')
= link_to set_location_member_path(@member), class: 'btn btn-block' do
= icon('fas', 'map-marker')
Set location
- if can?(:create, Notification) && current_member != @member
= link_to new_message_path(recipient_id: @member.id), class: 'btn btn-block' do

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.
- if @sets && !@sets.empty?
%p
= bootstrap_form_tag(url: new_photo_path, method: :get, layout: :inline) do |f|
= f.select :set, options_for_select(@sets, @current_set), label: "Choose a photo album"
= hidden_field_tag :type, @type
= hidden_field_tag :id, @id
= f.submit "Search", class: "btn btn-success"
%p
= 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", include_blank: true
= f.text_field :tag, value: @current_tag, label: "or search by tag"
= hidden_field_tag :type, @type
= hidden_field_tag :id, @id
= f.submit "Search", class: "btn btn-success"
- if @sets && @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
- if @owner
%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
%li.breadcrumb-item.active= link_to 'Plantings', plantings_path

View File

@@ -11,14 +11,14 @@
%p
%span.help-block
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.
.form-group
= f.label :crop_id, class: 'control-label col-md-2'
.col-md-8
= collection_select(:scientific_name, :crop_id, Crop.all.order(:name), :id,
:name, { selected: @scientific_name.crop_id || @crop.id },
= select(:scientific_name, :crop_id, Crop.order(:name).pluck(:name, :id),
{ selected: @scientific_name.crop_id || @crop.id },
class: 'form-control')
.form-group
= f.label :name, class: 'control-label col-md-2'

View File

@@ -32,7 +32,7 @@ csv.headers :id,
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 :from_location, s.owner.location

View File

@@ -1,7 +1,7 @@
- content_for :breadcrumbs do
- if @owner
%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
%li.breadcrumb-item.active= link_to 'Seeds', seeds_path

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.active_record.raise_in_transactional_callbacks = true
config.middleware.insert_before 0, Rack::Attack
config.middleware.insert_before 0, Rack::Cors do
allow do
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.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
# 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.
#
# 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
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
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

View File

@@ -63,6 +63,56 @@ en:
seed:
one: seed
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:
title:
title:
@@ -112,6 +162,9 @@ en:
forums:
index:
title: Forums
created: Forum was successfully created.
updated: Forum was successfully updated.
deleted: Forum was successfully deleted.
gardens:
created: Garden was successfully created.
deleted: Garden was successfully deleted.
@@ -208,6 +261,8 @@ en:
trade_to: Will trade to
unspecified: unspecified
view_all: View all seeds
added_to_stash: Successfully added %{crop} seed to your stash.
updated: Seed was successfully updated.
stats:
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}!
@@ -275,6 +330,21 @@ en:
links:
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:
edit_profile: Edit profile
index:
@@ -342,10 +412,22 @@ en:
progress_0_not_planted_yet: 'Progress: 0% - not planted yet'
posts:
write_blog_post: Write blog post
created: Post was successfully created.
updated: Post was successfully updated.
deleted: Post was deleted.
index:
title:
author_posts: "%{author} 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:
form:
trade_help: >
@@ -361,6 +443,7 @@ en:
owner_seeds: "%{owner} seeds"
save_seeds: Save seeds
string: "%{crop} seeds belonging to %{owner}"
added_to_stash: Successfully added %{crop} seed to your stash.
unauthorized:
create:
all: Please sign in or sign up to create a %{subject}.
@@ -373,3 +456,9 @@ en:
all: Not authorized to %{action} %{subject}.
read:
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

@@ -105,6 +105,7 @@ Rails.application.routes.draw do
resources :forums
resources :follows, only: %i(create destroy)
resources :blocks, only: %i(create destroy)
post 'likes' => 'likes#create'
delete 'likes' => 'likes#destroy'
@@ -112,6 +113,10 @@ Rails.application.routes.draw do
resources :timeline
resources :members, param: :slug do
member do
get :set_location
put :update_location
end
resources :gardens
resources :seeds
resources :plantings
@@ -121,6 +126,7 @@ Rails.application.routes.draw do
resources :follows
get 'followers' => 'follows#followers'
resources :blocks, only: %i(create destroy)
end
resources :messages

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class RenameOtherHandleToOtherUrlInMembers < ActiveRecord::Migration[6.0]
def change
rename_column :members, :other_handle, :other_url

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class CreateGardenCollaborators < ActiveRecord::Migration[7.2]
def change
create_table :garden_collaborators do |t|

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class CreateAustralianFoodClassificationData < ActiveRecord::Migration[5.2]
def change
create_table :australian_food_classification_data do |t|

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class MakeNotificationsPolymorphic < ActiveRecord::Migration[6.1]
def change
add_column :notifications, :notifiable_type, :string

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
class ChangeCommentsPolymorphic < ActiveRecord::Migration[7.2]
def change
add_column :comments, :commentable_type, :string

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