Compare commits

...

30 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
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
45 changed files with 642 additions and 183 deletions

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

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

View File

@@ -1,25 +1,31 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2026-04-25 16:44:38 UTC using RuboCop version 1.86.1.
# 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: 5
# Offense count: 8
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
@@ -89,9 +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'
- '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).
@@ -99,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
@@ -158,7 +173,7 @@ Lint/UselessConstantScoping:
Exclude:
- 'app/controllers/members_controller.rb'
# Offense count: 61
# Offense count: 65
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 295
@@ -180,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
@@ -195,7 +210,7 @@ Metrics/MethodLength:
Metrics/ModuleLength:
Max: 144
# Offense count: 8
# Offense count: 10
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 32
@@ -209,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:
@@ -221,34 +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: 298
# Offense count: 311
# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
Enabled: false
# Offense count: 36
# Offense count: 2
# Configuration parameters: IgnoredMetadata.
RSpec/DescribeClass:
Exclude:
- 'spec/models/harvest_prediction_spec.rb'
- 'spec/tasks/members_spec.rb'
# 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: 146
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowConsecutiveOneLiners.
RSpec/EmptyLineAfterExample:
Exclude:
- 'spec/controllers/crops_controller_spec.rb'
# 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: 32
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
@@ -283,27 +328,18 @@ RSpec/IncludeExamples:
- 'spec/views/photos/show.html.haml_spec.rb'
- 'spec/views/seeds/index.rss.haml_spec.rb'
# Offense count: 37
# 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/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
@@ -318,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:
@@ -329,11 +365,11 @@ RSpec/MultipleDescribes:
Exclude:
- 'spec/features/crops/crop_wranglers_spec.rb'
# Offense count: 191
# Offense count: 235
RSpec/MultipleExpectations:
Max: 19
# Offense count: 166
# Offense count: 171
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 16
@@ -344,24 +380,35 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NamedSubject:
Enabled: false
# Offense count: 109
# 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:
@@ -386,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
@@ -414,6 +461,13 @@ RSpec/VerifiedDoubles:
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/views/devise/shared/_links_spec.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Inferences.
RSpecRails/InferredSpecType:
Exclude:
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
# Offense count: 30
# Configuration parameters: Database.
# SupportedDatabases: mysql, postgresql
@@ -470,11 +524,25 @@ 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: 5
Rails/I18nLocaleTexts:
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.
Rails/InverseOf:
@@ -488,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:
@@ -614,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
@@ -623,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:
@@ -657,12 +738,13 @@ Style/FloatDivision:
Exclude:
- 'app/models/concerns/predict_planting.rb'
# Offense count: 1
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
Style/FrozenStringLiteralComment:
Exclude:
- 'db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb'
- 'spec/lib/haml/filters/growstuff_markdown_spec.rb'
# Offense count: 2
@@ -678,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).
@@ -736,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

@@ -1,4 +1,4 @@
FROM ruby:3.4.8-trixie
FROM ruby:3.4.9-trixie
# Install system dependencies
RUN apt-get update -qq && \

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
@@ -644,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)
@@ -886,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].present?
@owner = Member.find_by!(slug: params[:member_slug])
where['owner_id'] = @owner.id
@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

@@ -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

@@ -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

View File

@@ -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

@@ -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

@@ -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

View File

@@ -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)

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

@@ -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

View File

@@ -60,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)

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

@@ -79,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,
@@ -161,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)

View File

@@ -5,5 +5,8 @@
= 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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -456,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

@@ -113,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

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

@@ -0,0 +1,5 @@
class AddSendHarvestReminderToMembers < ActiveRecord::Migration[7.2]
def change
add_column :members, :send_harvest_reminder, :boolean, default: true, null: false
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -786,6 +786,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
t.string "facebook_handle"
t.string "bluesky_handle"
t.string "other_url"
t.boolean "send_harvest_reminder", default: true, null: false
t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true
t.index ["discarded_at"], name: "index_members_on_discarded_at"
t.index ["email"], name: "index_members_on_email", unique: true

View File

@@ -48,8 +48,23 @@ namespace :growstuff do
# Heroku scheduler only lets us run things daily, so this checks
# Send on Monday
if Time.zone.today.wday == 1
Member.confirmed.wants_reminders.each do |m|
Notifier.planting_reminder(m).deliver_now! unless m.plantings.active.empty?
Member.confirmed.wants_reminders.find_each do |m|
NotifierMailer.planting_reminder(m).deliver_later unless m.plantings.active.empty?
end
end
end
desc "Send harvest reminder email"
# usage: rake growstuff:send_harvest_reminders
task send_harvest_reminders: :environment do
# Heroku scheduler only lets us run things daily, so this checks
# Send on Wednesday
if Time.zone.today.wday == 3
Member.confirmed.wants_harvest_reminders.find_each do |m|
if m.plantings.active.any?(&:harvest_in_next_week?)
NotifierMailer.harvest_reminder(m).deliver_later
end
end
end
end

View File

@@ -7,6 +7,5 @@ namespace :search do
Planting.reindex
Harvest.reindex
Seed.reindex
Activity.reindex
end
end

View File

@@ -73,6 +73,21 @@ describe CropsController do
end
end
describe "GET CSV" do
let!(:tomato) { create(:tomato, en_wikipedia_url: "https://en.wikipedia.org/wiki/Tomato") }
before do
Crop.reindex
get :index, format: "csv"
end
it { is_expected.to be_successful }
it { expect(response.content_type).to eq("text/csv; charset=utf-8") }
it "contains tomato", pending: "not properly functional" do
expect(assigns(:crops)).not_to be_empty
expect(response.body).to include("tomato")
end
end
describe 'CREATE' do
subject { put :create, params: crop_params }

View File

@@ -10,6 +10,20 @@ describe PostsController do
{ author_id: member.id, subject: "blah", body: "blah blah" }
end
describe '#new' do
let(:crop) { create(:crop, name: 'Bush Bean') }
it 'pre-populates the body when crop_id is present' do
get :new, params: { crop_id: crop.slug }
expect(assigns(:post).body).to eq("[#{crop.name}](crop)")
end
it 'does not pre-populate the body when crop_id is absent' do
get :new
expect(assigns(:post).body).to be_nil
end
end
describe '#index' do
before do
create_list(:post, 100)

View File

@@ -21,5 +21,9 @@ FactoryBot.define do
description { "Stake tomato" }
planting
end
trait :reindex do
# Activity is not using elasticsearch anymore, so we don't need to reindex
end
end
end

View File

@@ -21,5 +21,9 @@ FactoryBot.define do
factory :forum_post do
forum
end
trait :reindex do
# Post is not using elasticsearch, but this trait is used in some tests
end
end
end

View File

@@ -0,0 +1,83 @@
require 'rails_helper'
RSpec.feature 'Set location', type: :feature do
let(:member) { FactoryBot.create(:member) }
before do
login_as(member, scope: :member)
end
scenario 'member sets their location by clicking on the map', js: true do
visit set_location_member_path(member)
# Test clicking on the map
page.execute_script("map.fire('click', { latlng: L.latLng(40.7128, -74.0060) })")
expect(find('#member_latitude').value).to eq('40.7128')
expect(find('#member_longitude').value).to eq('-74.006')
# Mock geocoding
geocoder_result = instance_double('Geocoder::Result::Nominatim',
city: 'New York',
town: nil,
village: nil,
hamlet: nil)
allow(Geocoder).to receive(:search).with([40.71, -74.01]).and_return([geocoder_result])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('New York')
expect(member.latitude).to eq(40.71)
expect(member.longitude).to eq(-74.01)
end
scenario 'member uses "Find my location"', js: true do
visit set_location_member_path(member)
# Mock browser's geolocation
page.execute_script("
navigator.geolocation.getCurrentPosition = function(success) {
var position = { coords: { latitude: 34.0522, longitude: -118.2437 } };
success(position);
}
")
click_button 'Find my location'
expect(find('#member_latitude').value).to eq('34.0522')
expect(find('#member_longitude').value).to eq('-118.2437')
# Mock geocoding
geocoder_result = instance_double('Geocoder::Result::Nominatim',
city: 'Los Angeles',
town: nil,
village: nil,
hamlet: nil)
allow(Geocoder).to receive(:search).with([34.05, -118.24]).and_return([geocoder_result])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('Los Angeles')
expect(member.latitude).to eq(34.05)
expect(member.longitude).to eq(-118.24)
end
scenario 'geocoding fails', js: true do
visit set_location_member_path(member)
page.execute_script("map.fire('click', { latlng: L.latLng(1.2345, 6.7890) })")
allow(Geocoder).to receive(:search).with([1.23, 6.79]).and_return([])
click_button 'Update location'
expect(page).to have_content('Location updated.')
member.reload
expect(member.location).to eq('Location near 1.23, 6.79')
expect(member.latitude).to eq(1.23)
expect(member.longitude).to eq(6.79)
end
end

View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'rails_helper'
describe NotifierMailer, type: :mailer do
let(:member) { create(:member) }
let(:mail) { NotifierMailer.harvest_reminder(member) }
it "has a greeting" do
expect(mail.body.encoded).to match "Hello"
end
context "when member has upcoming harvests" do
let(:crop) { create(:crop, median_days_to_first_harvest: 20) }
let!(:planting) { create(:planting, owner: member, crop: crop, planted_at: 15.days.ago) }
let(:plantings) { [planting] }
it "lists the upcoming harvest" do
expect(mail.body.encoded).to match "Upcoming harvests in your garden"
expect(mail.body.encoded).to match planting.crop.name
expect(mail.body.encoded).to match (Time.zone.today + 5.days).to_date.to_s
end
it "has an unsubscribe link" do
expect(mail.body.encoded).to match "Unsubscribe from harvest reminders"
end
end
end

View File

@@ -0,0 +1,42 @@
# frozen_string_literal: true
require 'rails_helper'
RSpec.describe Activity do
describe '.homepage_records' do
let(:member1) { create(:member) }
let(:member2) { create(:member) }
it 'returns the latest activities per owner' do
create(:activity, owner: member1, created_at: 2.days.ago)
latest_activity1 = create(:activity, owner: member1, created_at: 1.day.ago)
latest_activity2 = create(:activity, owner: member2, created_at: Time.current)
records = described_class.homepage_records(10)
expect(records).to contain_exactly(latest_activity1, latest_activity2)
end
it 'respects the limit' do
create(:activity, owner: member1)
create(:activity, owner: member2)
records = described_class.homepage_records(1)
expect(records.length).to eq(1)
end
end
describe 'active scope' do
it 'returns activities that are not finished' do
active_activity = create(:activity, finished: false)
finished_activity = create(:activity, finished: true)
expect(described_class.active).to include(active_activity)
expect(described_class.active).not_to include(finished_activity)
end
it 'treats nil finished as active' do
activity = create(:activity, finished: nil)
expect(described_class.active).to include(activity)
end
end
end

View File

@@ -0,0 +1,41 @@
# frozen_string_literal: true
require 'rails_helper'
describe "Harvest prediction logic" do
let(:crop) { create(:crop, median_days_to_first_harvest: 20) }
let(:planting) { create(:planting, crop: crop) }
describe "#harvest_in_next_week?" do
it "is true if predicted harvest is in 5 days" do
planting.planted_at = 15.days.ago
expect(planting.harvest_in_next_week?).to be true
end
it "is true if predicted harvest is today" do
planting.planted_at = 20.days.ago
expect(planting.harvest_in_next_week?).to be true
end
it "is true if predicted harvest is in 7 days" do
planting.planted_at = 13.days.ago
expect(planting.harvest_in_next_week?).to be true
end
it "is false if predicted harvest is in 8 days" do
planting.planted_at = 12.days.ago
expect(planting.harvest_in_next_week?).to be false
end
it "is false if predicted harvest was yesterday" do
planting.planted_at = 21.days.ago
expect(planting.harvest_in_next_week?).to be false
end
it "is false if there are already harvests" do
planting.planted_at = 15.days.ago
create(:harvest, planting: planting, owner: planting.owner, crop: planting.crop, harvested_at: 1.day.ago)
expect(planting.harvest_in_next_week?).to be false
end
end
end

View File

@@ -50,7 +50,6 @@ RSpec.configure do |config|
Photo.reindex
Planting.reindex
Seed.reindex
Activity.reindex
end
config.before(:suite) do

View File

@@ -90,13 +90,19 @@ describe 'layouts/_header.html.haml', type: "view" do
expect(rendered).to have_content 'Inbox'
expect(rendered).not_to match(/Inbox \d+/)
end
end
context 'has notifications' do
it 'shows inbox count' do
create(:notification, recipient: @member)
render
expect(rendered).to have_content 'Inbox 1'
end
context 'logged in, has notifications' do
before do
@member = create(:member)
create(:notification, recipient: @member)
sign_in @member
controller.stub(:current_user) { @member }
end
it 'shows inbox count' do
render
rendered.should have_content 'Inbox 1'
end
end
end