Compare commits

..

7 Commits

Author SHA1 Message Date
google-labs-jules[bot]
4b9763e1da feat: Add problem tracking feature
This commit introduces a new `Problem` model, analogous to `Crop`, to allow users to track problems they have on their plantings (e.g., aphids on tomatoes).

Key features:
- A new `Problem` model that can be curated by admins (`problem_wranglers`).
- Users can associate problems with their plantings and upload photos of the problems.
- Aggregated problem information is displayed on the crop detail page (e.g., "Problems: aphids (27), blight (13)").
- Users can mention problems in posts (e.g., `[aphids](problem)`), which automatically links to the problem's page.
- Admin functionality for reviewing and approving new problem suggestions.

Resolves merge conflict in app/controllers/plantings_controller.rb
2025-09-07 11:47:53 +00:00
Daniel O'Connor
8709ac6e8f Mark unique 2025-09-01 11:26:05 +00:00
Daniel O'Connor
d75f503b16 Add DB schema 2025-09-01 11:04:48 +00:00
Daniel O'Connor
1dfc5abca9 Add DB schema 2025-09-01 11:04:44 +00:00
Daniel O'Connor
cc97916bac Add problems table 2025-09-01 11:01:46 +00:00
Daniel O'Connor
2134b934f8 Merge branch 'dev' into feature/problems 2025-09-01 19:45:25 +09:30
google-labs-jules[bot]
c8cfb5d42a feat: Add problem tracking feature
This commit introduces a new `Problem` model, analogous to `Crop`, to allow you to track problems you have on your plantings (e.g., aphids on tomatoes).

Key features:
- A new `Problem` model that can be curated by admins (`problem_wranglers`).
- You can associate problems with your plantings and upload photos of the problems.
- Aggregated problem information is displayed on the crop detail page (e.g., "Problems: aphids (27), blight (13)").
- You can mention problems in posts (e.g., `[aphids](problem)`), which automatically links to the problem's page.
- Admin functionality for reviewing and approving new problem suggestions.
2025-09-01 07:52:48 +00:00
234 changed files with 3834 additions and 5060 deletions

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (admin/)
run: bundle exec rspec spec/features/admin/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (comments/)
run: bundle exec rspec spec/features/comments/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -101,9 +101,3 @@ jobs:
- name: Run rspec (conversations/)
run: bundle exec rspec spec/features/conversations/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (crops/)
run: bundle exec rspec spec/features/crops/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (gardens/)
run: bundle exec rspec spec/features/gardens/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -99,11 +99,4 @@ jobs:
run: bundle exec rails search:reindex
- name: Run rspec (harvests/)
run: bundle exec rspec spec/features/harvests/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots
run: bundle exec rspec spec/features/harvests/ -fd -t ~@flaky

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -99,11 +99,4 @@ jobs:
run: bundle exec rails search:reindex
- name: Run rspec (home/)
run: bundle exec rspec spec/features/home/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots
run: bundle exec rspec spec/features/home/ -fd -t ~@flaky

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -99,11 +99,4 @@ jobs:
run: bundle exec rails search:reindex
- name: Run rspec (members/)
run: bundle exec rspec spec/features/members/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots
run: bundle exec rspec spec/features/members/ -fd -t ~@flaky

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (places/)
run: bundle exec rspec spec/features/places/ -fd
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (plantings/)
run: bundle exec rspec spec/features/plantings/ -fd
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -1,4 +1,4 @@
name: CI Features - Posts
name: CI Features - Admin
on: [pull_request]
@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (posts/)
run: bundle exec rspec spec/features/posts/ -fd
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -100,10 +100,3 @@ jobs:
- name: Run rspec (seeds/)
run: bundle exec rspec spec/features/seeds/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -99,11 +99,4 @@ jobs:
run: bundle exec rails search:reindex
- name: Run rspec (timeline/)
run: bundle exec rspec spec/features/timeline/ -fd -t ~@flaky
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots
run: bundle exec rspec spec/features/timeline/ -fd -t ~@flaky

View File

@@ -38,7 +38,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -74,7 +74,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'
@@ -108,11 +108,4 @@ jobs:
run: bundle exec rspec spec/features/photos/ -fd
- name: Run rspec (rss/)
run: bundle exec rspec spec/features/rss/ -fd
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
with:
name: screenshots
path: tmp/screenshots
run: bundle exec rspec spec/features/rss/ -fd

View File

@@ -6,7 +6,7 @@ jobs:
contributors:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Install ruby version specified in .ruby-version
uses: ruby/setup-ruby@v1
with:
@@ -53,7 +53,7 @@ jobs:
steps:
- name: Checkout this repo
uses: actions/checkout@v5
uses: actions/checkout@v4
- name: Configure sysctl limits
run: |
@@ -89,7 +89,7 @@ jobs:
sudo apt-get -y install libpq-dev google-chrome-stable
- name: Install NodeJS
uses: actions/setup-node@v5
uses: actions/setup-node@v4
with:
node-version: '12'

View File

@@ -1,5 +1,5 @@
inherit_from: .rubocop_todo.yml
plugins:
require:
- rubocop-factory_bot
- rubocop-capybara
- rubocop-rails

View File

@@ -1,30 +1,18 @@
# This configuration was generated by
# `rubocop --auto-gen-config`
# on 2025-09-07 08:46:49 UTC using RuboCop version 1.80.2.
# on 2024-07-13 05:47:38 UTC using RuboCop version 1.65.0.
# 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: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: TreatCommentsAsGroupSeparators, ConsiderPunctuation.
Bundler/OrderedGems:
Exclude:
- 'Gemfile'
# Offense count: 231
# Configuration parameters: EnforcedStyle.
# SupportedStyles: link_or_button, strict
Capybara/ClickLinkOrButtonStyle:
Enabled: false
# Offense count: 18
Capybara/NegationMatcherAfterVisit:
Exclude:
- 'spec/features/crops/crop_detail_page_spec.rb'
- 'spec/features/crops/crop_wranglers_spec.rb'
- 'spec/features/gardens/gardens_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: 34
# Offense count: 39
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: DefaultSelector.
Capybara/RSpec/HaveSelector:
@@ -37,6 +25,7 @@ Capybara/RSpec/HaveSelector:
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/features/seeds/adding_seeds_spec.rb'
- 'spec/features/shared_examples/crop_suggest.rb'
- 'spec/helpers/application_helper_spec.rb'
- 'spec/support/feature_helpers.rb'
- 'spec/views/posts/show.html.haml_spec.rb'
@@ -67,7 +56,8 @@ FactoryBot/AssociationStyle:
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, ExplicitOnly.
# Configuration parameters: AutoCorrect, Include, EnforcedStyle, ExplicitOnly.
# Include: **/*_spec.rb, **/spec/**/*, **/test/**/*, **/features/support/factories/**/*.rb
# SupportedStyles: create_list, n_times
FactoryBot/CreateList:
Exclude:
@@ -76,88 +66,31 @@ FactoryBot/CreateList:
- 'spec/views/posts/index.html.haml_spec.rb'
# Offense count: 4
# Configuration parameters: MaxAmount.
# Configuration parameters: Include, MaxAmount.
# Include: **/*_spec.rb, **/spec/**/*, **/test/**/*, **/features/support/factories/**/*.rb
FactoryBot/ExcessiveCreateList:
Exclude:
- 'spec/controllers/posts_controller_spec.rb'
- 'spec/features/crops/show_spec.rb'
- 'spec/features/percy/percy_spec.rb'
# Offense count: 1144
# Offense count: 1127
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Include.
# Include: **/*_spec.rb, **/spec/**/*, **/test/**/*, **/features/support/factories/**/*.rb
FactoryBot/SyntaxMethods:
Enabled: false
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
Layout/EmptyLines:
Exclude:
- 'Gemfile'
# Offense count: 7
# This cop supports safe autocorrection (--autocorrect).
Layout/EmptyLinesAfterModuleInclusion:
Exclude:
- 'app/models/forum.rb'
- 'app/models/garden_type.rb'
- 'app/models/member.rb'
- 'app/models/plant_part.rb'
- 'app/models/role.rb'
- 'app/models/seed.rb'
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, AllowBeforeTrailingComments, ForceEqualSignAlignment.
Layout/ExtraSpacing:
Exclude:
- 'app/controllers/registrations_controller.rb'
- 'config/initializers/mailboxer.rb'
# Offense count: 4
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
# SupportedHashRocketStyles: key, separator, table
# SupportedColonStyles: key, separator, table
# SupportedLastArgumentHashStyles: always_inspect, always_ignore, ignore_implicit, ignore_explicit
Layout/HashAlignment:
Exclude:
- 'app/controllers/activities_controller.rb'
- 'lib/tasks/wikidata.rake'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, IndentationWidth.
# SupportedStyles: aligned, indented
Layout/LineEndStringConcatenationIndentation:
Exclude:
- 'app/models/seed.rb'
# Offense count: 3
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, IgnoreCopDirectives, AllowedPatterns, SplitStrings.
# Configuration parameters: Max, AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
Layout/LineLength:
Exclude:
- 'app/models/concerns/predict_planting.rb'
- 'app/models/member.rb'
- 'app/helpers/crops_helper.rb'
- 'db/seeds.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
Layout/RescueEnsureAlignment:
Exclude:
- 'app/helpers/event_helper.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowForAlignment, EnforcedStyleForExponentOperator, EnforcedStyleForRationalLiterals.
# SupportedStylesForExponentOperator: space, no_space
# SupportedStylesForRationalLiterals: space, no_space
Layout/SpaceAroundOperators:
Exclude:
- 'config/initializers/mailboxer.rb'
# Offense count: 4
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: RequireParenthesesForMethodChains.
Lint/AmbiguousRange:
@@ -165,20 +98,14 @@ Lint/AmbiguousRange:
- 'app/models/concerns/search_activities.rb'
- 'app/models/concerns/search_harvests.rb'
- 'app/models/concerns/search_plantings.rb'
- 'db/seeds.rb'
# Offense count: 2
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches, IgnoreDuplicateElseBranch.
# Configuration parameters: IgnoreLiteralBranches, IgnoreConstantBranches.
Lint/DuplicateBranch:
Exclude:
- 'app/models/harvest.rb'
- 'lib/actions/oauth_signup_action.rb'
# Offense count: 1
Lint/DuplicateMethods:
Exclude:
- 'app/models/planting.rb'
# Offense count: 8
# Configuration parameters: AllowComments, AllowEmptyLambdas.
Lint/EmptyBlock:
@@ -197,6 +124,12 @@ Lint/RedundantCopDisableDirective:
Exclude:
- 'db/migrate/20230313015323_create_active_storage_tables.active_storage.rb'
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Lint/RedundantDirGlobSort:
Exclude:
- 'spec/rails_helper.rb'
# Offense count: 1
# Configuration parameters: AllowComments, AllowNil.
Lint/SuppressedException:
@@ -204,18 +137,14 @@ Lint/SuppressedException:
- 'lib/tasks/testing.rake'
# Offense count: 7
# This cop supports safe autocorrection (--autocorrect).
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AutoCorrect.
Lint/UselessAssignment:
Exclude:
- 'config.rb'
- 'config/compass.rb'
# Offense count: 1
Lint/UselessConstantScoping:
Exclude:
- 'app/controllers/members_controller.rb'
# Offense count: 55
# Offense count: 52
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
Metrics/AbcSize:
Max: 151
@@ -224,42 +153,33 @@ Metrics/AbcSize:
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns, inherit_mode.
# AllowedMethods: refine
Metrics/BlockLength:
Max: 116
Max: 115
# Offense count: 9
# Offense count: 7
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 181
Max: 188
# Offense count: 8
# Offense count: 6
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/CyclomaticComplexity:
Max: 32
# Offense count: 73
# Offense count: 71
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
Metrics/MethodLength:
Max: 128
Max: 127
# Offense count: 2
# Configuration parameters: CountComments, CountAsOne.
Metrics/ModuleLength:
Max: 132
Max: 125
# Offense count: 7
# Offense count: 5
# Configuration parameters: AllowedMethods, AllowedPatterns.
Metrics/PerceivedComplexity:
Max: 32
# Offense count: 2
# Configuration parameters: Mode, AllowedMethods, AllowedPatterns, AllowBangMethods, WaywardPredicates.
# AllowedMethods: call
# WaywardPredicates: nonzero?
Naming/PredicateMethod:
Exclude:
- 'app/models/concerns/finishable.rb'
- 'app/models/seed.rb'
# Offense count: 3
RSpec/AnyInstance:
Exclude:
@@ -284,6 +204,7 @@ RSpec/DescribedClass:
# Offense count: 13
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AutoCorrect.
RSpec/EmptyExampleGroup:
Exclude:
- 'spec/controllers/authentications_controller_spec.rb'
@@ -306,10 +227,10 @@ RSpec/EmptyLineAfterExample:
Exclude:
- 'spec/models/ability_spec.rb'
# Offense count: 137
# Offense count: 140
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 27
Max: 25
# Offense count: 32
RSpec/ExpectInHook:
@@ -334,6 +255,7 @@ RSpec/HookArgument:
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AutoCorrect.
RSpec/HooksBeforeExamples:
Exclude:
- 'spec/features/crops/creating_a_crop_spec.rb'
@@ -354,12 +276,12 @@ RSpec/IndexedLet:
- 'spec/models/member_spec.rb'
- 'spec/views/forums/index.html.haml_spec.rb'
# Offense count: 719
# Offense count: 720
# Configuration parameters: AssignmentOnly.
RSpec/InstanceVariable:
Enabled: false
# Offense count: 42
# Offense count: 40
RSpec/LetSetup:
Enabled: false
@@ -385,14 +307,14 @@ RSpec/MultipleDescribes:
Exclude:
- 'spec/features/crops/crop_wranglers_spec.rb'
# Offense count: 149
# Offense count: 152
RSpec/MultipleExpectations:
Max: 19
# Offense count: 147
# Offense count: 138
# Configuration parameters: AllowSubject.
RSpec/MultipleMemoizedHelpers:
Max: 20
Max: 14
# Offense count: 133
# Configuration parameters: EnforcedStyle, IgnoreSharedExamples.
@@ -400,12 +322,12 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NamedSubject:
Enabled: false
# Offense count: 110
# Offense count: 111
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 6
# Offense count: 407
# Offense count: 403
# Configuration parameters: AllowedPatterns.
# AllowedPatterns: ^expect_, ^assert_
RSpec/NoExpectationExample:
@@ -436,13 +358,15 @@ RSpec/RepeatedExampleGroupBody:
# Offense count: 6
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AutoCorrect.
RSpec/ScatteredSetup:
Exclude:
- 'spec/features/percy/percy_spec.rb'
- 'spec/features/plantings/prediction_spec.rb'
# Offense count: 1
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata.
# Configuration parameters: Include, CustomTransform, IgnoreMethods, IgnoreMetadata.
# Include: **/*_spec.rb
RSpec/SpecFilePathFormat:
Exclude:
- 'spec/controllers/member_controller_spec.rb'
@@ -456,6 +380,8 @@ RSpec/StubbedMock:
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: constant, string
RSpec/VerifiedDoubleReference:
Exclude:
- 'spec/models/member_spec.rb'
@@ -485,36 +411,30 @@ RSpecRails/HaveHttpStatus:
RSpecRails/InferredSpecType:
Enabled: false
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: NilOrEmpty, NotPresent, UnlessPresent.
Rails/Blank:
Exclude:
- 'lib/tasks/wikidata.rake'
# Offense count: 29
# Configuration parameters: Database.
# Offense count: 28
# Configuration parameters: Database, Include.
# SupportedDatabases: mysql, postgresql
# Include: db/**/*.rb
Rails/BulkChangeTable:
Enabled: false
# Offense count: 4
# Configuration parameters: Include.
# Include: db/**/*.rb
Rails/CreateTableWithTimestamps:
Exclude:
- 'db/migrate/20150201052245_create_cms.rb'
- 'db/migrate/20171022032108_all_the_predictions.rb'
# Offense count: 3
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, AllowToTime.
# SupportedStyles: strict, flexible
Rails/Date:
Exclude:
- 'app/controllers/activities_controller.rb'
- 'app/mailers/notifier_mailer.rb'
- 'app/models/concerns/search_seeds.rb'
# Offense count: 12
# Offense count: 11
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedMethods, AllowedPatterns.
# AllowedMethods: order, limit, select, lock
@@ -525,40 +445,41 @@ Rails/FindEach:
- 'db/migrate/20171129041341_create_photographings.rb'
- 'db/migrate/20190130090437_add_crop_to_photographings.rb'
- 'db/migrate/20191119030244_cms_tags.rb'
- 'lib/tasks/wikidata.rake'
# Offense count: 2
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasAndBelongsToMany:
Exclude:
- 'app/models/member.rb'
- 'app/models/role.rb'
# Offense count: 5
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/HasManyOrHasOneDependent:
Exclude:
- 'app/models/member.rb'
# Offense count: 1
# Configuration parameters: Include.
# Include: spec/**/*.rb, test/**/*.rb
Rails/I18nLocaleAssignment:
Exclude:
- 'spec/features/locale_spec.rb'
# Offense count: 37
# Offense count: 33
Rails/I18nLocaleTexts:
Enabled: false
# Offense count: 3
# Configuration parameters: Include.
# Include: app/controllers/**/*.rb, app/mailers/**/*.rb
Rails/LexicallyScopedActionFilter:
Exclude:
- '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/crop.rb'
# Offense count: 2
Rails/OutputSafety:
Exclude:
@@ -573,13 +494,15 @@ Rails/PluralizationGrammar:
# Offense count: 4
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Include.
# Include: **/Rakefile, **/*.rake
Rails/RakeEnvironment:
Exclude:
- 'lib/tasks/hooks.rake'
- 'lib/tasks/i18n.rake'
- 'lib/tasks/testing.rake'
# Offense count: 8
# Offense count: 9
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedReceivers.
# AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone
@@ -590,6 +513,7 @@ Rails/RedundantActiveRecordAllMethod:
- 'app/controllers/forums_controller.rb'
- 'app/controllers/plant_parts_controller.rb'
- 'app/controllers/scientific_names_controller.rb'
- 'app/services/openfarm_service.rb'
- 'spec/features/percy/percy_spec.rb'
- 'spec/models/harvest_spec.rb'
@@ -604,6 +528,8 @@ Rails/RedundantPresenceValidationOnBelongsTo:
# Offense count: 15
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Include.
# Include: spec/controllers/**/*.rb, spec/requests/**/*.rb, test/controllers/**/*.rb, test/integration/**/*.rb
Rails/ResponseParsedBody:
Exclude:
- 'spec/controllers/api/v1/plantings_controller_spec.rb'
@@ -617,31 +543,29 @@ Rails/ResponseParsedBody:
- 'spec/requests/api/v1/seeds_request_spec.rb'
# Offense count: 9
# Configuration parameters: Include.
# Include: db/**/*.rb
Rails/ReversibleMigration:
Exclude:
- 'db/migrate/20130326092227_change_planted_at_to_date.rb'
- 'db/migrate/20191119020643_upgrade_cms.rb'
# Offense count: 3
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/RootPathnameMethods:
Exclude:
- 'app/controllers/crops_controller.rb'
- 'app/helpers/icons_helper.rb'
- 'config/application.rb'
# Offense count: 2
# 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/20240810160538_set_default_language_for_existing_alternate_names.rb'
# Offense count: 21
# Configuration parameters: Include.
# Include: db/**/*.rb
Rails/ThreeStateBooleanColumn:
Enabled: false
# Offense count: 6
# Configuration parameters: Include.
# Include: app/models/**/*.rb
Rails/UniqueValidationWithoutIndex:
Exclude:
- 'app/models/follow.rb'
@@ -659,13 +583,12 @@ Rails/WhereEquals:
- 'app/models/harvest.rb'
- 'app/models/planting.rb'
# Offense count: 3
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Rails/WhereRange:
Exclude:
- 'app/models/concerns/predict_planting.rb'
- 'app/models/garden.rb'
- 'app/models/seed.rb'
# Offense count: 1
Rake/MethodDefinitionInTask:
@@ -674,10 +597,8 @@ Rake/MethodDefinitionInTask:
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
# Configuration parameters: EnforcedStyle.
# SupportedStyles: nested, compact
# SupportedStylesForClasses: ~, nested, compact
# SupportedStylesForModules: ~, nested, compact
Style/ClassAndModuleChildren:
Exclude:
- 'lib/actions/oauth_signup_action.rb'
@@ -693,23 +614,7 @@ Style/CommentedKeyword:
- 'spec/models/photo_spec.rb'
- 'spec/models/planting_spec.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: trailing_conditional, ternary
Style/EmptyStringInsideInterpolation:
Exclude:
- 'app/helpers/auto_suggest_helper.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: left_coerce, right_coerce, single_coerce, fdiv
Style/FloatDivision:
Exclude:
- 'app/models/concerns/predict_planting.rb'
# Offense count: 11
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
@@ -717,34 +622,23 @@ Style/FrozenStringLiteralComment:
Exclude:
- 'config/initializers/new_framework_defaults_6_0.rb'
- 'db/migrate/20200801084007_add_foreign_key_constraint_to_active_storage_attachments_for_blob_id.active_storage.rb'
- 'db/migrate/20240716120000_add_social_media_to_members.rb'
- 'db/migrate/20240716120001_rename_other_handle_to_other_url_in_members.rb'
- 'db/migrate/20240929041435_create_garden_collaborators.rb'
- 'db/migrate/20250810120000_make_notifications_polymorphic.rb'
- 'db/migrate/20250824081313_change_comments_polymorphic.rb'
- 'db/migrate/20250901105232_add_source_to_seeds.rb'
- 'db/migrate/20250901110545_add_indexes_crops.rb'
- 'db/migrate/20250901130830_add_overall_rating_plantings.rb'
- 'spec/lib/haml/filters/growstuff_markdown_spec.rb'
# Offense count: 2
# Offense count: 3
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
Exclude:
- 'config/environments/production.rb'
- 'lib/tasks/gbif.rake'
- 'lib/tasks/openfarm.rake'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/HashFetchChain:
# Configuration parameters: AllowedMethods.
# AllowedMethods: nonzero?
Style/IfWithBooleanLiteralBranches:
Exclude:
- 'app/models/concerns/open_farm_data.rb'
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/IdenticalConditionalBranches:
Exclude:
- 'lib/actions/oauth_signup_action.rb'
- 'app/controllers/gardens_controller.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
@@ -766,14 +660,6 @@ Style/MutableConstant:
Exclude:
- 'app/models/activity.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, MinBodyLength, AllowConsecutiveConditionals.
# SupportedStyles: skip_modifier_ifs, always
Style/Next:
Exclude:
- 'lib/tasks/wikidata.rake'
# Offense count: 5
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle, AllowedMethods, AllowedPatterns.
@@ -790,12 +676,11 @@ Style/OpenStructUse:
Exclude:
- 'spec/helpers/event_helper_spec.rb'
# Offense count: 3
# Offense count: 2
# Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:
Exclude:
- 'app/helpers/application_helper.rb'
- 'app/models/concerns/member_newsletter.rb'
# Offense count: 1
@@ -812,40 +697,6 @@ Style/RedundantFetchBlock:
Exclude:
- 'config/puma.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/RedundantInterpolation:
Exclude:
- 'app/helpers/buttons_helper.rb'
# Offense count: 3
# This cop supports safe autocorrection (--autocorrect).
Style/RedundantRegexpEscape:
Exclude:
- 'app/models/member.rb'
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
Style/RegexpLiteral:
Exclude:
- 'app/models/member.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: implicit, explicit
Style/RescueStandardError:
Exclude:
- 'lib/tasks/wikidata.rake'
# Offense count: 4
# Configuration parameters: Max.
Style/SafeNavigationChainLength:
Exclude:
- 'app/models/ability.rb'
# Offense count: 2
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowModifier.
@@ -854,35 +705,17 @@ Style/SoleNestedConditional:
- 'app/controllers/application_controller.rb'
- 'app/controllers/messages_controller.rb'
# Offense count: 28
# Offense count: 24
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Mode.
Style/StringConcatenation:
Exclude:
- 'app/controllers/messages_controller.rb'
- 'app/helpers/application_helper.rb'
- 'app/helpers/buttons_helper.rb'
- 'app/models/photo.rb'
- 'config/initializers/rswag_api.rb'
- 'spec/helpers/gardens_helper_spec.rb'
- 'spec/helpers/seeds_helper_spec.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Exclude:
- 'config/initializers/mailboxer.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: .
# SupportedStyles: percent, brackets
Style/SymbolArray:
EnforcedStyle: percent
MinSize: 3
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowMethodsWithArguments, AllowedMethods, AllowedPatterns, AllowComments.

View File

@@ -12,7 +12,6 @@ submit the change with your pull request.
- Miles Gould / [pozorvlak](https://github.com/pozorvlak)
- Mackenzie Morgan / [maco](https://github.com/maco)
- Brenda Wallace / [br3nda](https://github.com/br3nda)
- Daniel O'Connor / [CloCkWeRX](https://github.com/CloCkWeRX)
## Contributors
@@ -69,6 +68,7 @@ submit the change with your pull request.
- Jym Paul Carandang / [jacarandang](https://github.com/jacarandang)
- Anthony Atkinson / [sha1sum](https://github.com/sha1sum)
- Terence Conquest / [twconquest](https://github.com/twconquest)
- Daniel O'Connor / [CloCkWeRX](https://github.com/CloCkWeRX)
- DV Dasari / [dv2](https://github.com/dv2)
- Eric Tillberg / [Thrillberg](https://github.com/Thrillberg)
- Lucas Nogueira / [lucasnogueira](https://github.com/lucasnogueira)
@@ -95,11 +95,10 @@ submit the change with your pull request.
- Ítalo Pires / [italopires](https://github.com/italopires)
- Bennett Zink / [bennett-zink](https://github.com/bennett-zink)
- Dominick Thornton / [domthor](https://github.com/domthor)
## Bots
### Security and Dependency Updates
- `codefactor-io[bot]`
- DeppBot / [deppbot](https://github.com/deppbot)
- `dependabot[bot]` [dependabot](https://github.com/dependabot-bot) / [dependabot-preview](https://github.com/apps/dependabot-preview)
- [google-labs-jules[bot]](https://github.com/apps/google-labs-jules)

View File

@@ -91,9 +91,10 @@ gem 'bootstrap-datepicker-rails'
# DRY-er easier bootstrap 4 forms
gem "bootstrap_form", ">= 4.5.0"
# For connecting to other services (eg Flickr)
# For connecting to other services (eg Twitter)
gem 'omniauth', '~> 1.3'
gem 'omniauth-flickr', '>= 0.0.15'
gem 'omniauth-twitter'
# Pretty charts
gem "chartkick"
@@ -178,6 +179,7 @@ group :development, :test do
gem 'dotenv-rails'
# cli utils
gem 'haml-i18n-extractor', require: false
gem 'haml_lint', '>= 0.25.1', require: false # Checks haml files for goodness
gem 'i18n-tasks', require: false # adds tests for finding missing and unused translations
gem 'rspectre', require: false # finds unused code in specs
@@ -200,5 +202,3 @@ group :travis do
end
gem "i18n_data", "~> 1.1"

View File

@@ -33,29 +33,29 @@ GEM
GEM
remote: https://rubygems.org/
specs:
actioncable (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
actioncable (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
nio4r (~> 2.0)
websocket-driver (>= 0.6.1)
zeitwerk (~> 2.6)
actionmailbox (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
actionmailbox (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
mail (>= 2.8.0)
actionmailer (7.2.2.2)
actionpack (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activesupport (= 7.2.2.2)
actionmailer (7.2.2.1)
actionpack (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activesupport (= 7.2.2.1)
mail (>= 2.8.0)
rails-dom-testing (~> 2.2)
actionpack (7.2.2.2)
actionview (= 7.2.2.2)
activesupport (= 7.2.2.2)
actionpack (7.2.2.1)
actionview (= 7.2.2.1)
activesupport (= 7.2.2.1)
nokogiri (>= 1.8.5)
racc
rack (>= 2.2.4, < 3.2)
@@ -64,15 +64,15 @@ GEM
rails-dom-testing (~> 2.2)
rails-html-sanitizer (~> 1.6)
useragent (~> 0.16)
actiontext (7.2.2.2)
actionpack (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
actiontext (7.2.2.1)
actionpack (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
globalid (>= 0.6.0)
nokogiri (>= 1.8.5)
actionview (7.2.2.2)
activesupport (= 7.2.2.2)
actionview (7.2.2.1)
activesupport (= 7.2.2.1)
builder (~> 3.1)
erubi (~> 1.11)
rails-dom-testing (~> 2.2)
@@ -84,25 +84,25 @@ GEM
activesupport (>= 7.1)
active_record_union (1.3.0)
activerecord (>= 4.0)
active_utils (3.6.0)
active_utils (3.5.0)
activesupport (>= 4.2)
i18n
activejob (7.2.2.2)
activesupport (= 7.2.2.2)
activejob (7.2.2.1)
activesupport (= 7.2.2.1)
globalid (>= 0.3.6)
activemodel (7.2.2.2)
activesupport (= 7.2.2.2)
activerecord (7.2.2.2)
activemodel (= 7.2.2.2)
activesupport (= 7.2.2.2)
activemodel (7.2.2.1)
activesupport (= 7.2.2.1)
activerecord (7.2.2.1)
activemodel (= 7.2.2.1)
activesupport (= 7.2.2.1)
timeout (>= 0.4.0)
activestorage (7.2.2.2)
actionpack (= 7.2.2.2)
activejob (= 7.2.2.2)
activerecord (= 7.2.2.2)
activesupport (= 7.2.2.2)
activestorage (7.2.2.1)
actionpack (= 7.2.2.1)
activejob (= 7.2.2.1)
activerecord (= 7.2.2.1)
activesupport (= 7.2.2.1)
marcel (~> 1.0)
activesupport (7.2.2.2)
activesupport (7.2.2.1)
base64
benchmark (>= 0.3)
bigdecimal
@@ -142,7 +142,7 @@ GEM
erubi (>= 1.0.0)
rack (>= 0.9.0)
rouge (>= 1.0.0)
bigdecimal (3.2.3)
bigdecimal (3.2.2)
bluecloth (2.2.0)
bonsai-elasticsearch-rails (7.0.1)
elasticsearch-model (< 8)
@@ -183,7 +183,7 @@ GEM
image_processing (~> 1.1)
marcel (~> 1.0.0)
ssrf_filter (~> 1.0)
chartkick (5.2.0)
chartkick (5.1.5)
childprocess (5.0.0)
coderay (1.1.3)
coercible (1.0.0)
@@ -198,7 +198,7 @@ GEM
comfy_bootstrap_form (4.0.9)
rails (>= 5.0.0)
concurrent-ruby (1.3.5)
connection_pool (2.5.4)
connection_pool (2.5.3)
crass (1.0.6)
crowdin-api (1.12.0)
open-uri (>= 0.1.0, < 0.2.0)
@@ -251,15 +251,15 @@ GEM
elasticsearch-transport (7.0.0)
faraday
multi_json
erb (5.0.2)
erb (5.0.1)
erubi (1.13.1)
erubis (2.7.0)
excon (1.2.5)
logger
execjs (2.10.0)
factory_bot (6.5.5)
factory_bot (6.5.4)
activesupport (>= 6.1.0)
factory_bot_rails (6.5.1)
factory_bot_rails (6.5.0)
factory_bot (~> 6.5)
railties (>= 6.1.0)
faker (3.5.2)
@@ -294,6 +294,12 @@ GEM
temple (>= 0.8.2)
thor
tilt
haml-i18n-extractor (0.5.9)
activesupport
haml
highline
tilt
trollop (= 1.16.2)
haml-rails (2.1.0)
actionpack (>= 5.1)
activesupport (>= 5.1)
@@ -335,8 +341,6 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.8, >= 1.8.1)
terminal-table (>= 1.5.1)
i18n_data (1.1.0)
simple_po_parser (~> 1.1)
icalendar (2.11.2)
base64
ice_cube (~> 0.16)
@@ -347,7 +351,7 @@ GEM
image_processing (1.12.2)
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.8.1)
io-console (0.8.0)
irb (1.15.2)
pp (>= 0.6.0)
rdoc (>= 4.0.0)
@@ -419,14 +423,14 @@ GEM
bigdecimal (~> 3.1)
net-http (0.6.0)
uri
net-imap (0.5.9)
net-imap (0.4.20)
date
net-protocol
net-pop (0.1.2)
net-protocol
net-protocol (0.2.2)
timeout
net-smtp (0.5.1)
net-smtp (0.5.0)
net-protocol
netrc (0.11.0)
nio4r (2.7.4)
@@ -436,7 +440,7 @@ GEM
nokogiri (1.18.9-x86_64-linux-gnu)
racc (~> 1.4)
oauth (0.5.6)
oj (3.16.11)
oj (3.16.10)
bigdecimal (>= 3.0)
ostruct (>= 0.2)
omniauth (1.9.2)
@@ -448,17 +452,20 @@ GEM
omniauth-oauth (1.1.0)
oauth
omniauth (~> 1.0)
omniauth-twitter (1.4.0)
omniauth-oauth (~> 1.1)
rack
open-uri (0.1.0)
orm_adapter (0.5.0)
ostruct (0.6.3)
ostruct (0.6.2)
parallel (1.27.0)
parser (3.3.9.0)
ast (~> 2.4.1)
racc
percy-capybara (5.0.0)
capybara (>= 3)
pg (1.6.2)
pg (1.6.2-x86_64-linux)
pg (1.6.1)
pg (1.6.1-x86_64-linux)
platform-api (3.8.0)
heroics (~> 0.1.1)
moneta (~> 1.0.0)
@@ -475,7 +482,7 @@ GEM
date
stringio
public_suffix (6.0.1)
puma (7.0.3)
puma (6.6.1)
nio4r (~> 2.0)
query_diet (0.7.2)
racc (1.8.1)
@@ -492,20 +499,20 @@ GEM
rackup (1.0.1)
rack (< 3)
webrick
rails (7.2.2.2)
actioncable (= 7.2.2.2)
actionmailbox (= 7.2.2.2)
actionmailer (= 7.2.2.2)
actionpack (= 7.2.2.2)
actiontext (= 7.2.2.2)
actionview (= 7.2.2.2)
activejob (= 7.2.2.2)
activemodel (= 7.2.2.2)
activerecord (= 7.2.2.2)
activestorage (= 7.2.2.2)
activesupport (= 7.2.2.2)
rails (7.2.2.1)
actioncable (= 7.2.2.1)
actionmailbox (= 7.2.2.1)
actionmailer (= 7.2.2.1)
actionpack (= 7.2.2.1)
actiontext (= 7.2.2.1)
actionview (= 7.2.2.1)
activejob (= 7.2.2.1)
activemodel (= 7.2.2.1)
activerecord (= 7.2.2.1)
activestorage (= 7.2.2.1)
activesupport (= 7.2.2.1)
bundler (>= 1.15.0)
railties (= 7.2.2.2)
railties (= 7.2.2.1)
rails-controller-testing (1.0.5)
actionpack (>= 5.0.1.rc1)
actionview (>= 5.0.1.rc1)
@@ -525,9 +532,9 @@ GEM
rails_stdout_logging
rails_serve_static_assets (0.0.5)
rails_stdout_logging (0.0.5)
railties (7.2.2.2)
actionpack (= 7.2.2.2)
activesupport (= 7.2.2.2)
railties (7.2.2.1)
actionpack (= 7.2.2.1)
activesupport (= 7.2.2.1)
irb (~> 1.13)
rackup (>= 1.0.0)
rake (>= 12.2)
@@ -543,11 +550,11 @@ GEM
rdoc (6.14.2)
erb
psych (>= 4.0.0)
recaptcha (5.21.1)
recaptcha (5.20.1)
redis-client (0.23.2)
connection_pool
regexp_parser (2.11.2)
reline (0.6.2)
regexp_parser (2.11.1)
reline (0.6.1)
io-console (~> 0.5)
responders (3.1.1)
actionpack (>= 5.2)
@@ -557,7 +564,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.4.2)
rexml (3.4.1)
rouge (4.1.2)
rspec (3.13.0)
rspec-core (~> 3.13.0)
@@ -575,7 +582,7 @@ GEM
rspec-mocks (3.13.5)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (8.0.2)
rspec-rails (8.0.1)
actionpack (>= 7.2)
activesupport (>= 7.2)
railties (>= 7.2)
@@ -601,7 +608,7 @@ GEM
rswag-ui (2.16.0)
actionpack (>= 5.2, < 8.1)
railties (>= 5.2, < 8.1)
rubocop (1.80.2)
rubocop (1.79.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)
@@ -621,7 +628,7 @@ GEM
rubocop-factory_bot (2.27.1)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rails (2.33.3)
rubocop-rails (2.32.0)
activesupport (>= 4.2.0)
lint_roller (~> 1.1)
rack (>= 1.1)
@@ -630,7 +637,7 @@ GEM
rubocop-rake (0.7.1)
lint_roller (~> 1.1)
rubocop (>= 1.72.1)
rubocop-rspec (3.7.0)
rubocop-rspec (3.6.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
@@ -641,7 +648,7 @@ GEM
ruby-units (4.1.0)
ruby-vips (2.2.1)
ffi (~> 1.12)
rubyzip (3.0.1)
rubyzip (2.4.1)
sass (3.7.4)
sass-listen (~> 4.0.0)
sass-listen (4.0.0)
@@ -655,17 +662,17 @@ GEM
sprockets (> 3.0)
sprockets-rails
tilt
scout_apm (5.7.1)
scout_apm (5.7.0)
parser
searchkick (5.3.1)
activemodel (>= 6.1)
hashie
securerandom (0.4.1)
selenium-webdriver (4.35.0)
selenium-webdriver (4.34.0)
base64 (~> 0.2)
logger (~> 1.4)
rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2, < 4.0)
rubyzip (>= 1.2.2, < 3.0)
websocket (~> 1.0)
sidekiq (7.3.9)
base64
@@ -673,7 +680,6 @@ GEM
logger
rack (>= 2.2.4)
redis-client (>= 0.22.2)
simple_po_parser (1.1.6)
sprockets (3.7.5)
base64
concurrent-ruby (~> 1.0)
@@ -688,16 +694,17 @@ GEM
temple (0.10.4)
terminal-table (4.0.0)
unicode-display_width (>= 1.1.1, < 4)
terser (1.2.6)
terser (1.2.5)
execjs (>= 0.3.0, < 3)
thor (1.4.0)
thread_safe (0.3.6)
tilt (2.6.1)
timecop (0.9.10)
timeout (0.4.3)
trollop (1.16.2)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
unicode-display_width (3.1.5)
unicode-display_width (3.1.4)
unicode-emoji (~> 4.0, >= 4.0.4)
unicode-emoji (4.0.4)
unicorn (6.1.0)
@@ -723,8 +730,7 @@ GEM
rack-test (>= 0.5.3)
webrick (1.9.1)
websocket (1.2.11)
websocket-driver (0.8.0)
base64
websocket-driver (0.7.6)
websocket-extensions (>= 0.1.0)
websocket-extensions (0.1.5)
will_paginate (4.0.1)
@@ -781,11 +787,11 @@ DEPENDENCIES
gibbon (~> 1.2.0)
gravatar-ultimate
haml
haml-i18n-extractor
haml-rails
haml_lint (>= 0.25.1)
hashie (>= 3.5.3)
i18n-tasks
i18n_data (~> 1.1)
icalendar
jquery-rails
jquery-ui-rails!
@@ -803,6 +809,7 @@ DEPENDENCIES
oj
omniauth (~> 1.3)
omniauth-flickr (>= 0.0.15)
omniauth-twitter
percy-capybara (~> 5.0.0)
pg
platform-api

View File

@@ -17,9 +17,7 @@ encourage participation from people of all backgrounds and skill levels.
## Want to contribute?
Don't ask to ask, the best way to get started is to fork the project, start a codespace and get hacking.
Dive on in and submit your PRs!
Vibe Coding is more than okay, just make sure you indicate if you have done so and ensure there are tests.
Dive on in and submit your PRs.
## Important links
@@ -37,10 +35,6 @@ frontend features. We welcome contributions -- see
* To set up your development environment, see [Getting started](https://github.com/Growstuff/growstuff/wiki/New-contributor-guide).
* You may also be interested in our [API](https://github.com/Growstuff/growstuff/wiki/API).
### For Home Automation enthusiasts
https://github.com/Growstuff/homeassistant-growstuff/
## For designers, writers, researchers, data wranglers, and other contributors
There are heaps of ways to get involved and contribute no matter what
@@ -68,3 +62,5 @@ For more information about this project, contact [info@growstuff.org](mailto:inf
Security Issues: If you find an authorization bypass or data breach, please contact our maintainers directly at [maintainers@growstuff.org](mailto:maintainers@growstuff.org).
You can also contact us on [Twitter](http://twitter.com/growstufforg/) or
[Facebook](https://www.facebook.com/pages/Growstuff/1531133417099494) or [Github](https://github.com/Growstuff/growstuff/issues)..

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View File

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@@ -1,13 +1,7 @@
.crop-icon {
height: 1em;
}
.card-footer {
.btn-group-vertical {
.btn {
text-wrap: initial
}
}
}
.crop-thumbnail {
.text {
bottom: 0;

View File

@@ -1,8 +1,6 @@
// stats shown on homepage. eg. "999 members..."
.stats {
a {
font-weight: bold;
}
font-weight: bold;
}
.crops,

View File

@@ -33,6 +33,13 @@
}
}
.location-not-set {
background-image: image-url("location-not-set.en.png");
background-position: center;
background-repeat: no-repeat;
height: 250px;
width: 100%;
}
.card {
.badge-location {
background-color: darken($blue, 10%);

View File

@@ -10,33 +10,9 @@
width: 100%;
}
#navbarSupportedContent {
ul {
flex-direction: column-reverse;
flex-wrap: nowrap;
li.nav-item {
display: block;
a {
display: grid;
grid-template-columns: 2em 1fr 2em;
}
a.dropdown-toggle::after {
width: 100%;
text-align: right;
}
}
}
}
.crop-actions {
flex-direction: column;
width: 100%;
a {
margin: auto;
.navbar .nav > li {
display: block;
}
}
.navbar .navbar-form {
padding-left: 0;

View File

@@ -10,7 +10,6 @@ body {
.navbar {
flex-wrap: nowrap;
align-items: flex-start
}
.navbar-brand {
.site-name {
@@ -132,8 +131,6 @@ section {
border-radius: 5%;
margin: 0.5em 0.5em 0.5em 0;
width: 200px;
align-items: stretch;
justify-content: space-between;
.img-card {
border-top-left-radius: 5%;
@@ -370,6 +367,9 @@ ul.thumbnail-buttons {
h1 {
font-size: 400%;
}
.stats a {
color: $black;
}
// signup widget on homepage
.signup {

View File

@@ -29,13 +29,9 @@ class ActivitiesController < DataController
def new
@activity = Activity.new(
owner: current_member,
owner: current_member,
due_date: Date.today
)
@activity.name = params[:name] if params[:name]
@activity.description = params[:description] if params[:description]
@activity.category = params[:category] if params[:category]
@activity.due_date = params[:due_date] if params[:due_date]
if params[:garden_id]
@activity.garden = Garden.find_by(
owner: current_member,
@@ -67,17 +63,7 @@ class ActivitiesController < DataController
end
def update
if @activity.update(activity_params)
if activity_params[:finished].present?
link = new_activity_path(
name: @activity.name,
garden_id: @activity.garden_id,
planting_id: @activity.planting_id,
due_date: 2.weeks.from_now.to_date
)
flash[:notice] = t('activities.finished_prompt_html', link: link).html_safe
end
end
@activity.update(activity_params)
respond_with @activity
end

View File

@@ -57,6 +57,6 @@ class AlternateNamesController < ApplicationController
private
def alternate_name_params
params.require(:alternate_name).permit(:crop_id, :name, :creator_id, :language)
params.require(:alternate_name).permit(:crop_id, :name, :creator_id)
end
end

View File

@@ -1,8 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class ActivitiesController < BaseController
end
end
end

View File

@@ -4,40 +4,6 @@ module Api
module V1
class BaseController < JSONAPI::ResourceController
abstract
protect_from_forgery with: :null_session
before_action :authenticate_member_from_token!
before_action :enforce_member_for_write_operations!, only: %i(create update destroy)
rescue_from CanCan::AccessDenied do
head :forbidden
end
def context
{
current_user: current_user,
current_ability: current_ability,
controller: self,
action: params[:action]
}
end
private
attr_reader :current_user
def enforce_member_for_write_operations!
head :unauthorized unless current_user
end
def authenticate_member_from_token!
authenticate_with_http_token do |token, _options|
auth = Authentication.find_by(token: token, provider: 'api')
if auth.present?
@current_user = auth.member
return true
end
end
end
end
end
end

View File

@@ -78,7 +78,6 @@ class ApplicationController < ActionController::Base
:tos_agreement,
# profile stuff
: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,
# update password

View File

@@ -13,55 +13,43 @@ class CommentsController < ApplicationController
end
def new
@commentable = find_commentable
@comment = Comment.new
if @commentable
@comments = @commentable.comments
@post = Post.find_by(id: params[:post_id])
if @post
@comments = @post.comments
respond_with(@comments)
else
redirect_to(request.referer || root_url,
alert: "Can't post a comment on a non-existent commentable")
alert: "Can't post a comment on a non-existent post")
end
end
def edit
# TODO: Why does this need a collection of comments?
@comments = @comment.commentable.comments
@commentable = @comment.commentable
@comments = @comment.post.comments
end
def create
@comment = Comment.new(comment_params)
@commentable = @comment.commentable
@comment.author = current_member
@comment.save
respond_with @comment, location: @commentable
respond_with @comment, location: @comment.post
end
def update
@comment.update(body: comment_params['body'])
respond_with @comment, location: @comment.commentable
respond_with @comment, location: @comment.post
end
def destroy
@commentable = @comment.commentable
@post = @comment.post
@comment.destroy
respond_with(@commentable)
respond_with(@post)
end
private
def find_commentable
return unless params[:comment]
if params[:comment][:commentable_type] == 'Photo'
Photo.find(params[:comment][:commentable_id])
elsif params[:comment][:commentable_type] == 'Post'
Post.find(params[:comment][:commentable_id])
end
end
def comment_params
params.require(:comment).permit(:body, :commentable_id, :commentable_type)
params.require(:comment).permit(:body, :post_id)
end
end

View File

@@ -39,6 +39,12 @@ class CropsController < ApplicationController
respond_with @crops
end
def openfarm
@crop = Crop.find(params[:crop_slug])
@crop.update_openfarm_data!
respond_with @crop, location: @crop
end
def gbif
@crop = Crop.find(params[:crop_slug])
@crop.update_gbif_data!
@@ -69,6 +75,7 @@ class CropsController < ApplicationController
end
def show
@problems = Problem.joins(plantings: :crop).where(crops: { id: @crop.id }).distinct
respond_to do |format|
format.html do
@posts = @crop.posts.order(created_at: :desc).paginate(page: params[:page])
@@ -131,6 +138,7 @@ class CropsController < ApplicationController
if @crop.approval_status_changed?(from: "pending", to: "approved")
notifier.deliver_now!
@crop.update_openfarm_data!
@crop.update_gbif_data!
end
else
@@ -159,7 +167,7 @@ class CropsController < ApplicationController
end
def save_crop_names
AlternateName.create!(names_params(:alt_name).map { |n| { name: n, creator_id: current_member.id, crop_id: @crop.id, language: "EN" } })
AlternateName.create!(names_params(:alt_name).map { |n| { name: n, creator_id: current_member.id, crop_id: @crop.id } })
ScientificName.create!(names_params(:sci_name).map { |n| { name: n, creator_id: current_member.id, crop_id: @crop.id } })
end
@@ -174,26 +182,26 @@ class CropsController < ApplicationController
def recreate_names(param_name, name_type)
return if params[param_name].blank?
@crop.send("#{name_type}_names").each(&:destroy)
destroy_names(name_type)
params[param_name].each_value do |value|
next if value.empty?
if name_type == 'alternate'
@crop.send("#{name_type}_names").create!(name: value, creator_id: current_member.id, language: "EN")
else
@crop.send("#{name_type}_names").create!(name: value, creator_id: current_member.id)
end
create_name!(name_type, value) unless value.empty?
end
end
def destroy_names(name_type)
@crop.send("#{name_type}_names").each(&:destroy)
end
def create_name!(name_type, value)
@crop.send("#{name_type}_names").create!(name: value, creator_id: current_member.id)
end
def crop_params
params.require(:crop).permit(
:name, :en_wikipedia_url,
:parent_id, :perennial,
:request_notes, :reason_for_rejection,
:rejection_notes,
:row_spacing, :spread, :height,
:sowing_method, :sun_requirements, :growing_degree_days,
scientific_names_attributes: %i(scientific_name _destroy id)
)
end
@@ -209,12 +217,12 @@ class CropsController < ApplicationController
def crop_json_fields
{
include: {
plantings: {
plantings: {
include: {
owner: { only: %i(id login_name location latitude longitude) }
}
},
scientific_names: { only: [:name] }, alternate_names: { only: %i(name language) }
scientific_names: { only: [:name] }, alternate_names: { only: [:name] }
}
}
end

View File

@@ -4,7 +4,7 @@ class GardensController < DataController
def index
@owner = Member.find_by(slug: params[:member_slug])
@show_all = params[:all] == '1'
@show_jump_to = params[:member_slug].present? || false
@show_jump_to = params[:member_slug].present? ? true : false
@gardens = @gardens.includes(:owner)
@gardens = @gardens.active unless @show_all
@@ -18,9 +18,8 @@ class GardensController < DataController
end
def show
@current_plantings = @garden.plantings.current.where.not(failed: true).includes(:crop, :owner).order(planted_at: :desc)
@current_plantings = @garden.plantings.current.includes(:crop, :owner).order(planted_at: :desc)
@current_activities = @garden.activities.current.includes(:owner).order(created_at: :desc)
@finished_activities = @garden.activities.finished.includes(:owner).order(created_at: :desc)
@finished_plantings = @garden.plantings.finished.includes(:crop)
@suggested_companions = Crop.approved.where(
id: CropCompanion.where(crop_a_id: @current_plantings.select(:crop_id)).select(:crop_b_id)
@@ -39,10 +38,7 @@ class GardensController < DataController
def create
@garden.owner_id = current_member.id
if @garden.save
link = new_activity_path(name: 'Weed the garden bed', garden_id: @garden.id, due_date: 2.weeks.from_now.to_date)
flash[:notice] = t('gardens.created_prompt_html', link: link).html_safe
end
flash[:notice] = I18n.t('gardens.created') if @garden.save
respond_with(@garden)
end

View File

@@ -16,6 +16,7 @@ class MembersController < ApplicationController
def show
@member = Member.confirmed.kept.find_by!(slug: params[:slug])
@twitter_auth = @member.auth('twitter')
@flickr_auth = @member.auth('flickr')
@posts = @member.posts

View File

@@ -28,7 +28,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
@authentication = action.establish_authentication(auth, member)
if action.member_created?
raise "Invalid provider" unless %w(flickr).index(auth['provider'].to_s)
raise "Invalid provider" unless %w(twitter flickr).index(auth['provider'].to_s)
session["devise.#{auth['provider']}_data"] = request.env["omniauth.auth"]
sign_in member

View File

@@ -21,7 +21,6 @@ class PhotosController < ApplicationController
def show
@crops = Crop.distinct.joins(:photo_associations).where(photo_associations: { photo: @photo })
@comment = Comment.new(commentable: @photo)
respond_with(@photo)
end

View File

@@ -37,7 +37,6 @@ class PlantingsController < DataController
@photos = @planting.photos.includes(:owner).order(date_taken: :desc)
@harvests = Harvest.search(where: { planting_id: @planting.id })
@current_activities = @planting.activities.current.includes(:owner).order(created_at: :desc)
@finished_activities = @planting.activities.finished.includes(:owner).order(created_at: :desc)
@matching_seeds = matching_seeds
@crop = @planting.crop
@@ -83,12 +82,7 @@ class PlantingsController < DataController
end
def update
if @planting.update(planting_params)
if planting_params[:finished].present? && @planting.garden.plantings.current.empty?
link = new_activity_path(name: 'Cultivate soil', garden_id: @planting.garden_id)
flash[:notice] = t('plantings.finished_prompt_html', link: link).html_safe
end
end
@planting.update(planting_params)
respond_with @planting
end
@@ -97,32 +91,6 @@ class PlantingsController < DataController
respond_with @planting, location: @planting.garden
end
def transplant
# The `load_and_authorize_resource` in DataController will handle finding the
# planting and authorizing the action.
# We still need to authorize the new garden
new_garden = Garden.find(params[:garden_id])
authorize! :update, new_garden
# Mark original planting as finished
@planting.update(finished: true, finished_at: Time.zone.now)
# Create a new planting
new_planting = @planting.dup
new_planting.garden = new_garden
new_planting.slug = nil # let friendly_id generate a new slug
new_planting.finished = false
new_planting.finished_at = nil
if new_planting.save
redirect_to edit_planting_path(new_planting), notice: 'Planting was successfully transplanted.'
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}"
end
end
private
def update_crop_medians
@@ -139,7 +107,7 @@ class PlantingsController < DataController
:crop_id, :description, :garden_id, :planted_at,
:parent_seed_id,
:quantity, :sunniness, :planted_from, :finished,
:finished_at, :failed, :overall_rating
:finished_at, :failed, problem_ids: []
)
end

View File

@@ -0,0 +1,76 @@
# frozen_string_literal: true
class ProblemsController < ApplicationController
before_action :authenticate_member!, except: %i(index show)
load_and_authorize_resource id_param: :slug, class: Problem
respond_to :html, :json
def index
@problems = Problem.approved.popular.paginate(page: params[:page])
@num_requested_problems = requested_problems.size if current_member
respond_with @problems
end
def requested
@requested = requested_problems.paginate(page: params[:page])
respond_with @requested
end
def show
@problem = Problem.friendly.find(params[:id])
@plantings = @problem.plantings.paginate(page: params[:page])
respond_with @problem
end
def new
@problem = Problem.new
respond_with @problem
end
def edit; end
def create
@problem = Problem.new(problem_params)
if current_member.role? :problem_wrangler
@problem.creator = current_member
else
@problem.requester = current_member
@problem.approval_status = "pending"
end
@problem.save
respond_with @problem
end
def update
if can?(:wrangle, @problem)
@problem.approval_status = 'rejected' if params.fetch("reject", false)
@problem.approval_status = 'approved' if params.fetch("approve", false)
end
@problem.update(problem_params)
respond_with @problem
end
def wrangle
@approval_status = params[:approval_status]
@problems = case @approval_status
when "pending"
Problem.pending_approval
when "rejected"
Problem.rejected
else
Problem.recent
end.paginate(page: params[:page])
@problem_wranglers = Role.problem_wranglers
respond_with @problems
end
private
def problem_params
params.require(:problem).permit(:name, :reason_for_rejection, :rejection_notes)
end
def requested_problems
current_member.requested_problems.pending_approval
end
end

View File

@@ -6,7 +6,8 @@ class RegistrationsController < Devise::RegistrationsController
prepend_before_action :check_captcha, only: [:create] # Change this to be any actions you want to protect with recaptcha.
def edit
@flickr_auth = current_member.auth('flickr')
@twitter_auth = current_member.auth('twitter')
@flickr_auth = current_member.auth('flickr')
render "edit"
end
@@ -38,12 +39,6 @@ class RegistrationsController < Devise::RegistrationsController
end
end
def regenerate_api_token
current_member.regenerate_api_token
set_flash_message :notice, :api_token_regenerated
redirect_to edit_member_registration_path + '#apps'
end
def destroy
if @member.valid_password?(params.require(:member)[:current_password])
@member.discard

View File

@@ -19,7 +19,9 @@ class SeedsController < DataController
where['parent_planting'] = @planting.id
end
where['tradeable_to'] = params[:tradeable_to] if params[:tradeable_to].present?
if params[:tradeable_to].present?
where['tradeable_to'] = params[:tradeable_to]
end
@show_all = (params[:all] == '1')
where['finished'] = false unless @show_all
@@ -43,7 +45,6 @@ class SeedsController < DataController
def new
@seed = Seed.new
@seed.source = 'my own seed saving'
if params[:planting_slug]
@planting = Planting.find_by(slug: params[:planting_slug])
@@ -57,8 +58,6 @@ class SeedsController < DataController
def create
@seed = Seed.new(seed_params)
@seed.source ||= 'my own seed saving'
@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
@@ -86,7 +85,7 @@ class SeedsController < DataController
:crop_id, :description, :quantity, :plant_before,
:parent_planting_id, :saved_at,
:days_until_maturity_min, :days_until_maturity_max,
:organic, :gmo, :source,
:organic, :gmo,
:heirloom, :tradable_to, :slug,
:finished, :finished_at
)

View File

@@ -21,32 +21,6 @@ module ApplicationHelper
classes
end
# Similar to Rails' time_ago_in_words, but gives a more standard
# output like "in 3 days" or "5 months ago".
# Also handles the case where from_time is a Date and to_time is a Date
# (in which case it just says "today" if they're the same date).
#
# NOTE: This is similar to distance_of_time_in_words but different enough
# that I think it's worth having a separate helper for it.
#
# from_time - the starting time (Time or Date)
# to_time - the ending time (Time or Date). Default: now (Time.zone.now)
# include_seconds - whether to include seconds in the calculation
#
# Returns a string like "in 3 days" or "5 months ago"
def standard_time_distance(from_time, to_time = 0, include_seconds = false)
return 'today' if from_time.is_a?(Date) && (from_time == to_time)
return 'now' if from_time == to_time
return "#{distance_of_time_in_words(from_time, to_time, include_seconds:)} ago" if from_time < to_time
"in #{distance_of_time_in_words(from_time, to_time, include_seconds:)}"
end
def count_github_contibutors
File.open(Rails.root.join('CONTRIBUTORS.md')).readlines.grep(/^-/).size
end
# Produces a cache key for uniquely identifying cached fragments.
def cache_key_for(klass, identifier = "all")
count = klass.count
@@ -58,7 +32,7 @@ module ApplicationHelper
# of HAML, Tilt, and dynamic compilation with interpolated ruby.
def markdownify(text)
translator = Haml::Filters::GrowstuffMarkdown.new
translator.expand_members!(translator.expand_crops!(text.to_s))
translator.expand_members!(translator.expand_problems!(translator.expand_crops!(text.to_s)))
end
#
@@ -76,6 +50,7 @@ module ApplicationHelper
uri.query = "&width=#{size}&height=#{size}" if uri.host == 'graph.facebook.com'
# TODO: Assess twitter - https://dev.twitter.com/overview/general/user-profile-images-and-banners
# TODO: Assess flickr - https://www.flickr.com/services/api/misc.buddyicons.html
return uri.to_s

View File

@@ -2,7 +2,6 @@
module ButtonsHelper
include IconsHelper
def garden_plant_something_button(garden, classes: "btn btn-default")
return unless can? :edit, garden
@@ -53,7 +52,7 @@ module ButtonsHelper
link_to t('buttons.mark_as_inactive'),
garden_path(garden, garden: { active: 0 }),
method: :put, class: classes,
data: { confirm: I18n.t('gardens.confirm_deactivate') }
data: { confirm: 'All plantings associated with this garden will be marked as finished. Are you sure?' }
end
def create_button(model_to_create, path, icon, label)
@@ -88,19 +87,6 @@ module ButtonsHelper
edit_button(edit_activity_path(activity), classes:)
end
def activity_copy_button(activity, classes: 'btn')
link_to new_activity_path(
name: activity.name,
description: activity.description,
category: activity.category,
garden_id: activity.garden_id,
planting_id: activity.planting_id,
due_date: activity.due_date
), class: classes do
copy_icon + ' ' + t('buttons.copy')
end
end
def activity_finish_button(activity, classes: 'btn btn-default btn-secondary')
return unless can?(:edit, activity) || activity.finished
@@ -111,7 +97,7 @@ module ButtonsHelper
end
def planting_finish_button(planting, classes: 'btn btn-default btn-secondary')
return unless can?(:edit, planting) || planting.finished || planting.failed
return unless can?(:edit, planting) || planting.finished
link_to planting_path(slug: planting.slug, planting: { finished: 1 }),
method: :put, class: "#{classes} append-date" do
@@ -119,15 +105,6 @@ module ButtonsHelper
end
end
def planting_failed_button(planting, classes: 'btn btn-default btn-secondary')
return unless can?(:edit, planting) || planting.finished || planting.failed
link_to planting_path(slug: planting.slug, planting: { failed: 1 }),
method: :put, class: "#{classes}" do
finished_icon + ' ' + t('buttons.mark_as_failed')
end
end
def seed_finish_button(seed, classes: 'btn btn-default')
return unless can?(:create, Planting) && seed.active
@@ -145,7 +122,7 @@ module ButtonsHelper
end
def planting_save_seeds_button(planting, classes: 'btn btn-default')
return unless can?(:edit, planting) && !planting.failed?
return unless can?(:edit, planting)
link_to new_planting_seed_path(planting_slug: planting.slug), class: classes do
seed_icon + ' ' + t('buttons.save_seeds')

View File

@@ -7,8 +7,6 @@ module EventHelper
def event_description(event)
render "#{event.event_type.pluralize}/description", event_model: resolve_model(event)
rescue ActionView::MissingTemplate
"#{event.event_type.humanize.downcase}d"
end
def resolve_model(event)

View File

@@ -59,10 +59,6 @@ module IconsHelper
image_icon 'delete'
end
def copy_icon
icon('far', 'copy')
end
def add_photo_icon
image_icon 'add-photo'
end

View File

@@ -43,14 +43,6 @@ module PlantingsHelper
(planting.first_harvest_predicted_at - Time.zone.today).to_i
end
# 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
Garden.active.where.not(id: planting.garden_id).where(id: garden_ids)
end
def days_from_now_to_last_harvest(planting)
return unless planting.planted_at.present? && planting.last_harvest_predicted_at.present?

View File

@@ -34,6 +34,8 @@ class Ability
# are wranglers or admins
cannot :read, Crop
can :read, Crop, approval_status: "approved"
cannot :read, Problem
can :read, Problem, approval_status: "approved"
# scientific names should only be viewable if associated crop is approved
cannot :read, ScientificName
can :read, ScientificName do |sn|
@@ -56,6 +58,8 @@ class Ability
# members can see even rejected or pending crops if they requested it
can :read, Crop, requester_id: member.id
can :requested, Crop # see list of crops they've requested
can :read, Problem, requester_id: member.id
can :requested, Problem
# managing your own user settings
can :update, Member, id: member.id
@@ -82,8 +86,14 @@ class Ability
can :gbif, Crop
end
if member.role? :problem_wrangler
can :wrangle, Problem
can :manage, Problem
end
# any member can create a crop provisionally
can :create, Crop
can :create, Problem
# can create & destroy their own authentications against other sites.
can :create, Authentication
@@ -111,10 +121,6 @@ class Ability
can :update, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :transplant, Planting, garden: { owner_id: member.id }
can :transplant, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :destroy, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end

View File

@@ -30,20 +30,4 @@ class Activity < ApplicationRecord
def to_s
name
end
def garden_name
garden&.name
end
def garden_slug
garden&.slug
end
def planting_name
planting&.crop&.name
end
def planting_slug
planting&.crop&.slug
end
end

View File

@@ -5,7 +5,6 @@ class AlternateName < ApplicationRecord
belongs_to :creator, class_name: 'Member', inverse_of: :created_alternate_names
validates :name, presence: true
validates :crop, presence: true
validates :language, presence: true
after_commit :reindex

View File

@@ -2,27 +2,26 @@
class Comment < ApplicationRecord
belongs_to :author, class_name: 'Member', inverse_of: :comments
belongs_to :commentable, polymorphic: true, counter_cache: true
# validates :body, presence: true
belongs_to :post, counter_cache: true
scope :post_order, -> { order(created_at: :asc) } # for display on post page
after_create do
recipient = commentable.author.id
recipient = post.author.id
sender = author.id
# don't send notifications to yourself
if recipient != sender
Notification.create(
recipient_id: recipient,
sender_id: sender,
subject: "#{author} commented on #{commentable.subject}",
subject: "#{author} commented on #{post.subject}",
body:,
notifiable: commentable
post_id: post.id
)
end
end
def to_s
"#{author.login_name} commented on #{commentable.subject}"
"#{author.login_name} commented on #{post.subject}"
end
end

View File

@@ -8,12 +8,7 @@ module Finishable
scope :current, -> { where.not(finished: true) }
def active
# Plantings can fail. At the moment, activities and seeds cannot.
if respond_to?(:failed)
!finished && !failed
else
!finished
end
!finished
end
end
end

View File

@@ -4,10 +4,22 @@ module OpenFarmData
extend ActiveSupport::Concern
included do
def update_openfarm_data!
OpenfarmService.new.update_crop(self)
end
def of_photo
fetch_attr('main_image_path')
end
def height
fetch_attr('height')
end
def spread
fetch_attr('spread')
end
def svg_icon
icon = fetch_attr('svg_icon')
return icon if icon.present?
@@ -23,18 +35,38 @@ module OpenFarmData
fetch_attr('description')
end
def row_spacing
fetch_attr('row_spacing')
end
def common_names
fetch_attr('common_names')
end
def guides_count
fetch_attr('guides_count')
end
def binomial_name
fetch_attr('binomial_name')
end
def sowing_method
fetch_attr('sowing_method')
end
def main_image_path
fetch_attr('main_image_path')
end
def sun_requirements
fetch_attr('sun_requirements')
end
def growing_degree_days
fetch_attr('growing_degree_days')
end
def processing_pictures
fetch_attr('processing_pictures')
end

View File

@@ -38,7 +38,7 @@ module PredictHarvest
# status
def harvest_time?
return false if crop.perennial || finished || failed
return false if crop.perennial || finished
# We have harvests but haven't finished
harvests.size.positive? ||

View File

@@ -8,12 +8,12 @@ module PredictPlanting
before_save :calculate_lifespan
def calculate_lifespan
self.lifespan = (planted_at.present? && finished_at.present? && !failed? ? finished_at - planted_at : nil)
self.lifespan = (planted_at.present? && finished_at.present? ? finished_at - planted_at : nil)
end
# dates
def finish_predicted_at
if planted_at.blank? || failed?
if planted_at.blank?
nil
elsif crop.median_lifespan.present?
planted_at + crop.median_lifespan.days
@@ -34,18 +34,15 @@ module PredictPlanting
end
def actual_lifespan
return unless planted_at.present? && finished_at.present? && !failed?
return unless planted_at.present? && finished_at.present?
(finished_at - planted_at).to_i
end
def age_in_days
return if planted_at.blank?
return if failed?
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
end
@@ -53,9 +50,9 @@ module PredictPlanting
Rails.cache.fetch("#{cache_key_with_version}/percentage_grown", expires_in: 8.hours) do
if finished?
100
elsif !planted? || failed?
elsif !planted?
0
elsif crop.perennial || (finish_predicted_at.nil? && finished_at.nil?) # This covers future dated finished_at that hasn't occurrred yet.
elsif crop.perennial || finish_predicted_at.nil?
nil
else
calculate_percentage_grown
@@ -74,7 +71,7 @@ module PredictPlanting
end
def late?
crop.annual? && !finished && !failed &&
crop.annual? && !finished &&
planted_at.present? &&
finish_predicted_at.present? &&
finish_predicted_at <= Time.zone.today
@@ -94,9 +91,9 @@ module PredictPlanting
private
def calculate_percentage_grown
return 0 if age_in_days.to_i < 0
return 0 if age_in_days < 0
percent = (age_in_days.to_f / expected_lifespan.to_f) * 100
percent = (age_in_days / expected_lifespan.to_f) * 100
(percent > 100 ? 100 : percent)
end
end

View File

@@ -9,9 +9,7 @@ module SearchActivities
mappings: {
properties: {
active: { type: :boolean },
created_at: { type: :integer },
updated_at: { type: :integer },
due_date: { type: :date }
created_at: { type: :integer }
}
}
@@ -25,10 +23,8 @@ module SearchActivities
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

View File

@@ -59,8 +59,7 @@ module SearchSeeds
search('*', limit:,
where: {
finished: false,
tradable: true,
_or: [{ plant_before: nil }, { plant_before: { lt: Date.today } }]
tradable: true
},
boost_by: [:created_at],
load: false)

View File

@@ -17,6 +17,7 @@ class Crop < ApplicationRecord
has_many :scientific_names, dependent: :delete_all
has_many :alternate_names, dependent: :delete_all
has_many :plantings, dependent: :destroy
has_many :problems, through: :plantings
has_many :seeds, dependent: :destroy
has_many :harvests, dependent: :destroy
has_many :photo_associations, dependent: :delete_all, inverse_of: :crop
@@ -90,7 +91,7 @@ class Crop < ApplicationRecord
def popular_plant_parts
PlantPart.joins(:harvests)
.where("crop_id = ?", id)
.order(count_harvests_id: :desc)
.order("count_harvests_id DESC")
.group("plant_parts.id", "plant_parts.name")
.count("harvests.id")
end

View File

@@ -59,7 +59,7 @@ class CsvImporter
alternate_names.split(/,\s*/).each do |name|
altname = AlternateName.find_by(name:, crop: @crop)
altname ||= AlternateName.create! name:, crop: @crop, language: "EN", creator: cropbot
altname ||= AlternateName.create! name:, crop: @crop, creator: cropbot
@crop.alternate_names << altname
end
end

View File

@@ -10,8 +10,7 @@ class Follow < ApplicationRecord
recipient_id: followed_id,
sender_id: follower_id,
subject: "#{follower.login_name} is now following you",
body: "#{follower.login_name} just followed you on #{ENV.fetch('GROWSTUFF_SITE_NAME', nil)}. ",
notifiable: self
body: "#{follower.login_name} just followed you on #{ENV.fetch('GROWSTUFF_SITE_NAME', nil)}. "
)
end
end

View File

@@ -5,7 +5,6 @@ class Garden < ApplicationRecord
include Geocodable
include PhotoCapable
include Ownable
friendly_id :garden_slug, use: %i(slugged finders)
has_many :plantings, dependent: :destroy
@@ -45,7 +44,6 @@ class Garden < ApplicationRecord
.where.not(gardens: { latitude: nil })
.where.not(gardens: { longitude: nil })
}
AREA_UNITS_VALUES = {
"square metres" => "square metre",
"square feet" => "square foot",

View File

@@ -24,20 +24,6 @@ class Member < ApplicationRecord
has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient
has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification"
has_many :authentications, dependent: :destroy
has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy
def api_token?
api_token.present?
end
def regenerate_api_token
api_token.destroy if api_token?
create_api_token(
provider: 'api',
uid: id,
token: SecureRandom.hex(16)
)
end
has_many :photos, inverse_of: :owner
has_many :likes, dependent: :destroy
@@ -56,6 +42,10 @@ class Member < ApplicationRecord
inverse_of: :requester
has_many :created_crops, class_name: 'Crop', foreign_key: 'creator_id', dependent: :nullify,
inverse_of: :creator
has_many :requested_problems, class_name: 'Problem', foreign_key: 'requester_id', dependent: :nullify,
inverse_of: :requester
has_many :created_problems, class_name: 'Problem', foreign_key: 'creator_id', dependent: :nullify,
inverse_of: :creator
has_many :created_alternate_names, class_name: 'AlternateName', foreign_key: 'creator_id', inverse_of: :creator
has_many :created_scientific_names, class_name: 'ScientificName', foreign_key: 'creator_id', inverse_of: :creator
@@ -105,9 +95,6 @@ class Member < ApplicationRecord
uniqueness: {
case_sensitive: false
}
validates :website_url, format: { with: /\Ahttps?:\/\//, message: "must start with http:// or https://" }, allow_blank: true
validates :other_url, format: { with: /\Ahttps?:\/\//, message: "must start with http:// or https://" }, 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
#
# Triggers

View File

@@ -3,7 +3,7 @@
class Notification < ApplicationRecord
belongs_to :sender, class_name: 'Member', inverse_of: :sent_notifications
belongs_to :recipient, class_name: 'Member', inverse_of: :notifications
belongs_to :notifiable, polymorphic: true
belongs_to :post, optional: true
validates :subject, length: { maximum: 255 }

View File

@@ -8,7 +8,6 @@ class Photo < ApplicationRecord
PHOTO_CAPABLE = %w(Garden Planting Harvest Seed Post Crop).freeze
has_many :photo_associations, dependent: :delete_all, inverse_of: :photo
has_many :comments, as: :commentable, dependent: :destroy
# This doesn't work, ActiveRecord tries to use the polymoriphic photographable
# relationship instead.
@@ -46,8 +45,7 @@ class Photo < ApplicationRecord
flickr = owner.flickr
info = flickr.photos.getInfo(photo_id: source_id)
licenses = flickr.photos.licenses.getInfo
license = licenses.find { |l| l.id.to_i == info.license.to_i }
Rails.logger.error("Cannot find license: " + [info.license, licenses].inspect) unless license
license = licenses.find { |l| l.id == info.license }
{
title: calculate_title(info),
license_name: license.name,
@@ -85,14 +83,6 @@ class Photo < ApplicationRecord
"#{title} by #{owner.login_name}"
end
def subject
title
end
def author
owner
end
def flickr_photo_id
source_id if source == 'flickr'
end

View File

@@ -24,6 +24,8 @@ class Planting < ApplicationRecord
belongs_to :crop, counter_cache: true
has_many :harvests, dependent: :destroy
has_many :activities, dependent: :destroy
has_many :planting_problems, dependent: :destroy
has_many :problems, through: :planting_problems
#
# Ancestry of food
@@ -43,8 +45,7 @@ class Planting < ApplicationRecord
.where.not(gardens: { latitude: nil })
.where.not(gardens: { longitude: nil })
}
scope :active, -> { where(finished: false, failed: false).where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
scope :failed, -> { where(failed: true) }
scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
scope :annual, -> { joins(:crop).where(crops: { perennial: false }) }
scope :perennial, -> { joins(:crop).where(crops: { perennial: true }) }
scope :interesting, -> { has_photos.one_per_owner.order(planted_at: :desc) }
@@ -73,7 +74,6 @@ class Planting < ApplicationRecord
validates :crop, presence: true, approved: { message: "must be present and exist in our database" }
validate :finished_must_be_after_planted
validate :owner_must_match_garden_owner
validate :cannot_be_finished_and_failed
validates :quantity, allow_nil: true, numericality: {
only_integer: true, greater_than_or_equal_to: 0
}
@@ -83,9 +83,6 @@ class Planting < ApplicationRecord
validates :planted_from, allow_blank: true, inclusion: {
in: PLANTED_FROM_VALUES, message: "%<value>s is 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
}
def planting_slug
[
@@ -101,11 +98,7 @@ class Planting < ApplicationRecord
end
def finished?
(finished || (finished_at.present? && finished_at <= Time.zone.today)) && !failed?
end
def failed?
failed
finished || (finished_at.present? && finished_at <= Time.zone.today)
end
def planted?
@@ -129,10 +122,6 @@ class Planting < ApplicationRecord
private
def cannot_be_finished_and_failed
errors.add(:failed, "can't be true if planting is also 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

View File

@@ -0,0 +1,8 @@
# frozen_string_literal: true
class PlantingProblem < ApplicationRecord
include PhotoCapable
belongs_to :planting
belongs_to :problem
end

View File

@@ -3,7 +3,6 @@
class Post < ApplicationRecord
extend FriendlyId
include Likeable
friendly_id :author_date_subject, use: %i(slugged finders)
include PhotoCapable
@@ -11,15 +10,17 @@ class Post < ApplicationRecord
# Relationships
belongs_to :author, class_name: 'Member', inverse_of: :posts
belongs_to :forum, optional: true
has_many :comments, as: :commentable, dependent: :destroy
has_many :comments, dependent: :destroy
has_many :crop_posts, dependent: :delete_all
has_many :crops, through: :crop_posts
has_many :notifications, as: :notifiable, dependent: :destroy
has_many :problem_posts, dependent: :delete_all
has_many :problems, through: :problem_posts
after_create :send_notification
#
# Triggers
after_save :update_crop_posts_association
after_save :update_problem_posts_association
default_scope { joins(:author).merge(Member.kept) } # Ensures the owner still exists
@@ -77,6 +78,17 @@ class Post < ApplicationRecord
end
end
def update_problem_posts_association
problems.clear
# look for problems mentioned in the post. eg. [aphids](problem)
body.scan(Haml::Filters::GrowstuffMarkdown::PROBLEM_REGEX) do |_m|
problem_name = Regexp.last_match(1)
problem = Problem.case_insensitive_name(problem_name).first
# create association
problems << problem if problem && problems.exclude?(problem)
end
end
def send_notification
recipients = []
sender = author.id
@@ -97,7 +109,6 @@ class Post < ApplicationRecord
Notification.create(
recipient_id:,
sender_id: sender,
notifiable: self,
subject: "#{author} mentioned you in their post #{subject}",
body:
)

88
app/models/problem.rb Normal file
View File

@@ -0,0 +1,88 @@
# frozen_string_literal: true
class Problem < ApplicationRecord
extend FriendlyId
include PhotoCapable
include SearchCrops # Note: This might need to be adapted to SearchProblems
friendly_id :name, use: %i(slugged finders)
##
## Relationships
belongs_to :creator, class_name: 'Member', optional: true, inverse_of: :created_problems
belongs_to :requester, class_name: 'Member', optional: true, inverse_of: :requested_problems
has_many :planting_problems, dependent: :delete_all
has_many :plantings, through: :planting_problems
has_many :problem_posts, dependent: :delete_all
has_many :posts, through: :problem_posts, dependent: :delete_all
##
## Scopes
scope :recent, -> { approved.order(created_at: :desc) }
scope :popular, -> { approved.order(Arel.sql("plantings_count desc, lower(name) asc")) }
scope :pending_approval, -> { where(approval_status: "pending") }
scope :approved, -> { where(approval_status: "approved") }
scope :rejected, -> { where(approval_status: "rejected") }
scope :interesting, -> { approved.has_photos }
scope :has_photos, -> { includes(:photos).where.not(photos: { id: nil }) }
##
## Validations
validates :reason_for_rejection, presence: true, if: :rejected?
validate :must_be_rejected_if_rejected_reasons_present
validate :must_have_meaningful_reason_for_rejection
validates :name, uniqueness: { scope: :approval_status }, if: :pending?
def to_s
name
end
def to_param
slug
end
def pending?
approval_status == "pending"
end
def approved?
approval_status == "approved"
end
def rejected?
approval_status == "rejected"
end
def approval_statuses
%w(rejected pending approved)
end
def reasons_for_rejection
["already in database", "not a pest or disease", "not enough information", "other"]
end
def rejection_explanation
return rejection_notes if reason_for_rejection == "other"
reason_for_rejection
end
def self.case_insensitive_name(name)
where(["lower(problems.name) = :value", { value: name.downcase }])
end
private
def must_be_rejected_if_rejected_reasons_present
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")
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\"")
end
end

View File

@@ -0,0 +1,6 @@
# frozen_string_literal: true
class ProblemPost < ApplicationRecord
belongs_to :problem
belongs_to :post
end

View File

@@ -8,7 +8,7 @@ class Role < ApplicationRecord
has_and_belongs_to_many :members
class << self
%i(crop_wranglers admins).each do |method|
%i(crop_wranglers admins problem_wranglers).each do |method|
define_method method do
slug = method.to_s.singularize.dasherize
Role.where(slug:).try(:first).try(:members)

View File

@@ -12,8 +12,6 @@ class Seed < ApplicationRecord
ORGANIC_VALUES = ['certified organic', 'non-certified organic', 'conventional/non-organic', 'unknown'].freeze
GMO_VALUES = ['certified GMO-free', 'non-certified GMO-free', 'GMO', 'unknown'].freeze
HEIRLOOM_VALUES = %w(heirloom hybrid unknown).freeze
SOURCE_VALUES = ['seed catalogue', 'retail outlet', 'seed bank or similar institution',
'traded from another person', 'my own seed saving', 'other/unknown'].freeze
#
# Relationships
@@ -46,9 +44,6 @@ class Seed < ApplicationRecord
validates :heirloom, allow_blank: false,
inclusion: { in: HEIRLOOM_VALUES, message: "You must say whether the seeds" \
"are heirloom, hybrid, or unknown" }
validates :source, allow_blank: true,
inclusion: { in: SOURCE_VALUES, message: "You must say where the seeds are from," \
"or that you don't know" }
#
# Delegations
@@ -64,7 +59,6 @@ class Seed < ApplicationRecord
scope :has_location, -> { joins(:owner).where.not('members.location': nil) }
scope :recent, -> { order(created_at: :desc) }
scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
scope :expired, -> { active.where('plant_before < ?', Time.zone.today) }
def tradable
tradable_to != 'nowhere'

View File

@@ -1,29 +0,0 @@
# frozen_string_literal: true
module Api
module V1
class ActivityResource < BaseResource
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :garden
has_one :planting
attribute :name
attribute :description
attribute :category
attribute :finished
attribute :due_date
filter :owner
filter :owner_id
filter :garden
filter :garden_id
filter :planting
filter :planting_id
filter :category
end
end
end

View File

@@ -3,7 +3,8 @@
module Api
module V1
class CropResource < BaseResource
immutable # TODO: Re-evaluate this later
immutable
filter :approval_status, default: 'approved'
has_many :plantings

View File

@@ -3,22 +3,13 @@
module Api
module V1
class GardenResource < BaseResource
before_create do
@model.owner = context[:current_user]
end
immutable
has_one :owner, class_name: 'Member'
has_many :plantings
has_many :photos
attribute :name
filter :owner
filter :owner_id
filter :active
filter :garden_type
filter :location
filter :slug
end
end
end

View File

@@ -3,17 +3,11 @@
module Api
module V1
class HarvestResource < BaseResource
before_save do
@model.owner = context[:current_user]
@model.crop_id = @model.planting.crop_id if @model.planting_id
@model.harvested_at = Time.zone.now if @model.harvested_at.blank?
@model.plant_part = PlantPart.first
end
immutable
has_one :crop
has_one :planting
has_one :owner, class_name: 'Member'
# has_one :plant_part
has_many :photos
attribute :harvested_at
@@ -22,15 +16,6 @@ module Api
attribute :weight_quantity
attribute :weight_unit
attribute :si_weight
filter :owner
filter :owner_id
filter :crop
filter :crop_id
filter :planting
filter :planting_id
filter :plant_part
filter :harvested_at
end
end
end

View File

@@ -3,10 +3,7 @@
module Api
module V1
class PhotoResource < BaseResource
immutable # TODO: Re-evaluate this.
before_create do
@model.owner = context[:current_user]
end
immutable
has_one :owner, class_name: 'Member'
has_many :plantings

View File

@@ -3,9 +3,7 @@
module Api
module V1
class PlantingResource < BaseResource
before_create do
@model.owner = context[:current_user]
end
immutable
has_one :garden
has_one :crop
@@ -15,7 +13,6 @@ module Api
attribute :slug
attribute :planted_at
attribute :failed
attribute :finished
attribute :finished_at
attribute :quantity
@@ -38,13 +35,11 @@ module Api
filter :owner
filter :owner_id
filter :finished
filter :active, apply: ->(records, _value, _options) { records.active }
filter :failed, apply: ->(records, _value, _options) { records.failed }
filter :sunniness
filter :perennial, apply: ->(records, _value, _options) { records.perennial }
attribute :percentage_grown
delegate :percentage_grown, to: :@model
def percentage_grown
@model.percentage_grown
end
attribute :crop_name
attribute :crop_slug

View File

@@ -3,9 +3,7 @@
module Api
module V1
class SeedResource < BaseResource
before_create do
@model.owner = context[:current_user]
end
immutable
has_one :owner, class_name: 'Member'
has_one :crop
@@ -19,15 +17,6 @@ module Api
attribute :organic
attribute :gmo
attribute :heirloom
filter :owner
filter :owner_id
filter :crop
filter :crop_id
filter :tradable_to
filter :organic
filter :gmo
filter :heirloom
end
end
end

View File

@@ -1,16 +1,6 @@
# frozen_string_literal: true
class BaseResource < JSONAPI::Resource
immutable
abstract
[:create, :update, :remove].each do |action|
set_callback action, :before, :authorize
end
# Check authorisation for write operations.
# NOTE: At a later time, we may require API tokens for READ operations.
def authorize
# context[:action] is simply context[:controller].params[:action]
context[:current_ability].authorize! context[:action].to_sym, @model
end
end

View File

@@ -0,0 +1,108 @@
# frozen_string_literal: true
BASE = 'https://openfarm.cc/api/v1/'
# BASE = 'http://127.0.0.1:3000/api/v1/'
class OpenfarmService
def initialize
@cropbot = Member.find_by(login_name: 'cropbot')
end
def import!
Crop.all.order(updated_at: :desc).each do |crop|
Rails.logger.debug { "#{crop.id}, #{crop.name}" }
update_crop(crop) if crop.valid?
end
end
def update_crop(crop)
openfarm_record = fetch(crop.name)
if openfarm_record.present? && openfarm_record.is_a?(String)
Rails.logger.info(openfarm_record)
elsif openfarm_record.present? && openfarm_record.fetch('data', false)
crop.update! openfarm_data: openfarm_record.fetch('data', false)
save_companions(crop, openfarm_record)
save_photos(crop)
else
Rails.logger.debug "\tcrop not found on Open Farm"
crop.update!(openfarm_data: false)
end
end
def save_companions(crop, openfarm_record)
companions = openfarm_record.fetch('data').fetch('relationships').fetch('companions').fetch('data')
crops = openfarm_record.fetch('included', []).select { |rec| rec["type"] == 'crops' }
CropCompanion.transaction do
companions.each do |com|
companion_crop_hash = crops.detect { |c| c.fetch('id') == com.fetch('id') }
companion_crop_name = companion_crop_hash.fetch('attributes').fetch('name').downcase
companion_crop = Crop.where('lower(name) = ?', companion_crop_name).first
companion_crop = Crop.create!(name: companion_crop_name, requester: @cropbot, approval_status: "pending") if companion_crop.nil?
crop.companions << companion_crop unless crop.companions.where(id: companion_crop.id).any?
end
end
end
def save_photos(crop)
pictures = fetch_pictures(crop.name)
pictures.each do |picture|
data = picture.fetch('attributes')
Rails.logger.debug(data)
next unless data.fetch('image_url').start_with? 'http'
next if Photo.find_by(source_id: picture.fetch('id'), source: 'openfarm')
photo = Photo.new(
source_id: picture.fetch('id'),
source: 'openfarm',
owner: @cropbot,
thumbnail_url: data.fetch('thumbnail_url'),
fullsize_url: data.fetch('image_url'),
title: 'Open Farm photo',
license_name: 'No rights reserved',
link_url: "https://openfarm.cc/en/crops/#{name_to_slug(crop.name)}"
)
if photo.valid?
Photo.transaction do
photo.save
PhotoAssociation.find_or_create_by! photo:, photographable: crop
end
Rails.logger.debug { "\t saved photo #{photo.id} #{photo.source_id}" }
else
Rails.logger.warn "Photo not valid"
end
end
end
def fetch(name)
conn.get("crops/#{name_to_slug(name)}.json").body
rescue NoMethodError
Rails.logger.debug "error fetching crop"
Rails.logger.debug "BODY: "
Rails.logger.debug body
end
def name_to_slug(name)
CGI.escape(name.gsub(' ', '-').downcase)
end
def fetch_all(page)
conn.get("crops.json?page=#{page}").body.fetch('data', {})
end
def fetch_pictures(name)
body = conn.get("crops/#{name_to_slug(name)}/pictures.json").body
body.fetch('data', false)
rescue StandardError
Rails.logger.debug "Error fetching photos"
Rails.logger.debug []
end
private
def conn
Faraday.new BASE do |conn|
conn.response :json, content_type: /\bjson$/
conn.adapter Faraday.default_adapter
end
end
end

View File

@@ -18,20 +18,10 @@ class TimelineService
.union_all(photos_query)
.union_all(seeds_query)
.union_all(activities_query)
.union_all(likes_query)
.where.not(event_at: nil)
.order(event_at: :desc)
end
def self.likes_query
Like
.select("likes.id",
"'like' as event_type",
"likes.created_at as event_at",
"likes.member_id as owner_id",
"null as crop_id")
end
def self.activities_query
Activity.select(
:id,

View File

@@ -3,7 +3,6 @@
%a#activity-actions-button.btn.btn-info.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", type: "button", href: '#'} Actions
.dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "planting-actions-button"}
= activity_edit_button(activity, classes: 'dropdown-item')
= activity_copy_button(activity, classes: 'dropdown-item')
- if activity.active
= activity_finish_button(activity, classes: 'dropdown-item')
.dropdown-divider

View File

@@ -20,24 +20,16 @@
- if can? :destroy, activity
.dropdown-divider
= delete_button(activity, classes: 'dropdown-item text-danger')
.card-body
= link_to activity_path(slug: activity.slug) do
= link_to activity_path(slug: activity.slug) do
.card-body.text-center
%h4= activity.name
- if activity.due_date
%small.due-date{title: activity.due_date}
= standard_time_distance(activity.due_date.to_date, Time.zone.now.to_date)
%div
%small.text-justify{title: activity.description}= activity.description.truncate(150)
%p
%ul.list-unstyled
- if activity.garden_name && activity.garden_slug
%li
%small= link_to activity.garden_name, garden_path(slug: activity.garden_slug)
- if activity.planting_name && activity.planting_slug
%li
%small= link_to activity.planting_name, planting_path(slug: activity.planting_slug)
.text-center= activity.description
- if activity.garden
.text-center= activity.garden
- if activity.planting
.text-center= activity.planting
.card-footer
%small.chip.member-chip
= link_to member_path(slug: activity.owner_slug) do
= activity.owner_login_name
.float-right
%span.chip.member-chip
= link_to member_path(slug: activity.owner_slug) do
= activity.owner_login_name

View File

@@ -27,13 +27,13 @@
.row
.col-md-4
= f.collection_radio_buttons(:garden_id, @activity.owner.gardens.active.order_by_name,
= f.collection_radio_buttons(:garden_id, @activity.owner.gardens.active,
:id, :name,
label: 'Is this for a specific garden?')
= link_to "Add a garden.", new_garden_path
.col-md-4
= f.collection_radio_buttons(:planting_id, @activity.owner.plantings.active.recent,
= f.collection_radio_buttons(:planting_id, @activity.owner.plantings.active,
:id, :crop_name,
label: 'Is this for a specific planting?')
= link_to "Add a planting.", new_planting_path
@@ -47,7 +47,7 @@
.row
.col-md-6
= f.check_box :finished, label: t('buttons.mark_as_finished')
= f.check_box :finished, label: 'Mark as finished'
%span.help-block= t('.finish_helper')
.card-footer

View File

@@ -31,8 +31,7 @@
= f.label :name, class: 'control-label col-md-2'
.col-md-8
= f.text_field :name, class: 'form-control'
.col-md-8
= f.select :language, I18nData.languages.map {|code, name| [name.split(";").first, code] }, class: 'form-control'
.form-group
.form-actions.col-md-offset-2.col-md-8
= f.submit 'Save', class: 'btn btn-primary'

View File

@@ -1 +1 @@
#{link_to 'commented', event_model} on #{link_to event_model.commentable, event_model.commentable}
#{link_to 'commented', event_model} on #{link_to event_model.post, event_model.post}

View File

@@ -1,4 +1,3 @@
- @comment ||= Comment.new(commentable: @commentable)
.card.col-md-8.col-lg-7.mx-auto.float-none.white.z-depth-1.py-2.px-2
.card-body
- if content_for? :title
@@ -15,13 +14,13 @@
%li= msg
.md-form
= f.text_area :body, rows: 6, class: 'form-control md-textarea', autofocus: 'autofocus', required: true, pattern: '\w+'
= f.text_area :body, rows: 6, class: 'form-control md-textarea', autofocus: 'autofocus'
= f.label :body, "Your comment:"
%span.help-block
= render partial: "shared/markdown_help"
.actions.text-right
= f.submit 'Post comment', class: 'btn btn-primary'
.field
= f.hidden_field :commentable_id, value: @commentable.id
= f.hidden_field :commentable_type, value: @commentable.class.name
- if defined?(@post)
.field
= f.hidden_field :post_id, value: @post.id

View File

@@ -2,6 +2,6 @@
%p
Editing comment on
= link_to @comment.commentable.subject, @comment.commentable
= link_to @comment.post.subject, @comment.post
= render 'form', locals: { comment: @comment, commentable: @comment.commentable }
= render 'form'

View File

@@ -5,18 +5,17 @@
%link= comments_url
- @comments.each do |comment|
%item
%title Comment by #{comment.author.login_name} on #{comment.commentable.subject}
%title Comment by #{comment.author.login_name} on #{comment.post.subject}
%description
:escaped_markdown
<p>
Comment on
#{ link_to comment.commentable.subject, polymorphic_url(comment.commentable) }
#{ link_to comment.post.subject, post_url(comment.post) }
</p>
:escaped_markdown
#{ strip_tags markdownify(comment.body) }
%pubdate= comment.created_at.to_fs(:rfc822)
%link= polymorphic_url(comment.commentable)
%link= post_url(comment.post)
%guid= comment_url(comment)

View File

@@ -1,17 +1,11 @@
= content_for :title, "New comment"
- if @commentable.is_a?(Post)
%section.blog-post
.card.post{ id: "post-#{@commentable.id}" }
.card-header
%h2.display-3= @commentable.subject
.card-body= render "posts/single", post: @commentable || @comment.commentable, subject: true
- elsif @commentable.is_a?(Photo)
%section.blog-post
.card.photo{ id: "photo-#{@commentable.id}" }
.card-header
%h2.display-3= @commentable.subject
.card-body= render "photos/card", photo: @commentable || @comment.commentable, subject: true
= render partial: "comments/comments", locals: { commentable: @commentable || @comment.commentable }
%section.blog-post
.card.post{ id: "post-#{@post.id}" }
.card-header
%h2.display-3= @post.subject
.card-body= render "posts/single", post: @post || @comment.post, subject: true
= render 'form', locals: { comment: @comment, commentable: @commentable || @comment.commentable }
= render partial: "posts/comments", locals: { post: @post || @comment.post }
= render 'form'

View File

@@ -1,6 +1,6 @@
- if crop.approved? && signed_in?
.btn-group.crop-actions{"aria-label" => "Crop Actions", role: "group"}
.btn-group{"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)

View File

@@ -5,7 +5,7 @@
- if can? :edit, an
.dropdown.planting-actions
%a#crop-actions-altnames.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-bs-toggle" => "dropdown", :type => "button", :href => '#'}
= "#{an.name} (#{an.language})"
= an.name
.dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "crop-actions-altnames"}
- if can? :edit, an
= link_to edit_alternate_name_path(an), class: 'dropdown-item' do
@@ -16,7 +16,7 @@
= delete_icon
= t('.delete')
- else
.badge= "#{an.name} (#{an.language})"
.badge= an.name
%p.text-right

View File

@@ -25,9 +25,9 @@
Last harvest expected
%strong= crop.median_days_to_last_harvest
days after planting
- if member_signed_in?
.card-footer
.d-flex.btn-group-vertical
= 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)
- if member_signed_in?
.card-footer
.d-flex.justify-content-between
= 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)

View File

@@ -41,14 +41,6 @@
= f.radio_button(:perennial, true, label: "Perennial")
%span.help-block Living more than two years
%h2 OpenFarm Data
= f.number_field :row_spacing, label: 'Row Spacing (cm)', min: 0
= f.number_field :spread, label: 'Spread (cm)', min: 0
= f.number_field :height, label: 'Height (cm)', min: 0
= f.text_field :sowing_method
= f.text_field :sun_requirements
= f.number_field :growing_degree_days, min: 0
- unless @crop.approved?
= link_to 'Search wikipedia', "https://en.wikipedia.org/w/index.php?search=#{@crop.name}", target: '_blank'
= f.url_field :en_wikipedia_url, id: "en_wikipedia_url", label: 'Wikipedia URL'

View File

@@ -12,7 +12,7 @@
#{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)}
%small #{distance_of_time_in_words(harvest.harvested_at, Time.zone.now)} ago.
%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

@@ -1,33 +0,0 @@
- if crop.row_spacing || crop.spread || crop.height || crop.sowing_method || crop.sun_requirements || crop.growing_degree_days
= cute_icon
.card
.card-body
%h4 OpenFarm Data
%ul.list-group.list-group-flush
- if crop.row_spacing
%li.list-group-item
%strong Row Spacing:
= crop.row_spacing
cm
- if crop.spread
%li.list-group-item
%strong Spread:
= crop.spread
cm
- if crop.height
%li.list-group-item
%strong Height:
= crop.height
cm
- if crop.sowing_method
%li.list-group-item
%strong Sowing Method:
= crop.sowing_method
- if crop.sun_requirements
%li.list-group-item
%strong Sun Requirements:
= crop.sun_requirements
- if crop.growing_degree_days
%li.list-group-item
%strong Growing Degree Days:
= crop.growing_degree_days

View File

@@ -0,0 +1,5 @@
- if crop.guides_count.present? && crop.guides_count.positive?
%p
There are
= link_to "https://openfarm.cc/en/crops/#{CGI.escape @crop.name.gsub(' ', '-').downcase}" do
#{crop.guides_count} growing guides on Open Farm

View File

@@ -10,6 +10,10 @@
= edit_icon
= t('.edit')
= link_to crop_openfarm_path(crop), method: :post, class: 'dropdown-item' do
= icon 'far', 'update'
Fetch data from OpenFarm
= link_to crop_gbif_path(crop), method: :post, class: 'dropdown-item' do
= icon 'far', 'update'
Fetch data from GBIF

View File

@@ -30,6 +30,15 @@
- @crop.companions.each do |companion|
= render 'crops/tiny', crop: companion
- if @problems.any?
%section.problems
%h2 Problems
- @problems.group_by(&:name).each do |problem_name, problems|
- problem = problems.first
= link_to problem_path(problem) do
= problem_name
%span.badge.badge-secondary= problems.size
%section.photos
= cute_icon
= render 'crops/photos', crop: @crop
@@ -74,6 +83,7 @@
.card-body
%h4 How to grow #{@crop.name.pluralize}
= render 'grown_for', crop: @crop
= render 'planting_advice', crop: @crop
- if @crop.parent
%hr/
%p.parent-crop
@@ -111,8 +121,6 @@
= render 'harvests', crop: @crop
= render 'find_seeds', crop: @crop
= render 'openfarm_data', crop: @crop
= cute_icon
.card
.card-body
@@ -125,6 +133,13 @@
= icon 'fas', 'external-link-alt'
Wikipedia (English)
%li.list-group-item
= link_to "https://openfarm.cc/en/crops/#{CGI.escape @crop.name.gsub(' ', '-')}",
class: 'card-link',
target: "_blank",
rel: "noopener noreferrer" do
= icon 'fas', 'external-link-alt'
OpenFarm - Growing guide
%li.list-group-item
= link_to "https://www.gardenate.com/plant/#{CGI.escape @crop.name}",
target: "_blank",
@@ -141,14 +156,6 @@
= icon 'fas', 'external-link-alt'
Google
%li.list-group-item
= link_to 'https://chat.openai.com/?model=gpt-4o&prompt=' + CGI.escape(['How do I grow', @crop.name, "and what grows well with it? What should I plant the next season if practicing crop rotation? Explain why and add links to your sources"].join(' ')),
target: "_blank",
class: 'card-link',
rel: "noopener noreferrer" do
= icon 'fas', 'external-link-alt'
ChatGPT
%li.list-group-item
= link_to "https://wikihow.com/wikiHowTo?search=#{CGI.escape "grow #{@crop.name}" }",
target: "_blank",

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