Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel O'Connor
9c4238e601 Merge branch 'dev' into feature-use-test-queue 2026-04-26 02:03:56 +09:30
Daniel O'Connor
75a2ec89e0 Merge branch 'dev' into feature-use-test-queue 2026-04-24 00:02:00 +09:30
google-labs-jules[bot]
bd5769cf4f feat: Use test-queue to parallelize RSpec tests
This change introduces the `test-queue` gem to the project to enable parallel execution of the RSpec test suite. The CI configuration in `.github/workflows/ci.yml` has been updated to use a single `test-queue` command instead of multiple, sequential `rspec` commands. This will help to speed up the CI process by running tests concurrently.

The `--fail-fast` option has been removed from the RSpec command, as it is not recommended when running tests in parallel. This ensures that all tests are run, providing a complete picture of the test suite's status.
2025-11-29 03:40:46 +00:00
178 changed files with 975 additions and 2818 deletions

View File

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

1
.github/FUNDING.yml vendored
View File

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

View File

@@ -108,23 +108,5 @@ jobs:
run: bundle exec rails db:prepare run: bundle exec rails db:prepare
- name: Run rspec (lib) - name: Run tests with test-queue
run: bundle exec rspec spec/lib/ -fd --fail-fast run: bundle exec test-queue rspec spec -fd
- name: Run rspec (services)
run: bundle exec rspec spec/services/ -fd --fail-fast
- name: Run rspec (models)
run: bundle exec rspec spec/models/ -fd --fail-fast
- name: Run rspec (controllers)
run: bundle exec rspec spec/controllers/ -fd --fail-fast
- name: Run rspec (views)
run: bundle exec rspec spec/views/ -fd --fail-fast
- name: Run rspec (routing)
run: bundle exec rspec spec/routing/ -fd --fail-fast
- name: Run rspec (request)
run: bundle exec rspec spec/requests/ -fd --fail-fast

View File

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

View File

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

View File

@@ -1 +1 @@
3.4.9 3.4.8

View File

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

View File

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

View File

@@ -116,8 +116,6 @@ gem 'xmlrpc' # fixes rake error - can be removed if not needed later
gem 'puma' gem 'puma'
gem 'rack-attack'
gem 'loofah', '>= 2.19.1' gem 'loofah', '>= 2.19.1'
gem 'rack-protection', '>= 2.0.1' gem 'rack-protection', '>= 2.0.1'
@@ -198,6 +196,7 @@ group :test do
gem 'rails-controller-testing' gem 'rails-controller-testing'
gem "rspec-rebound" gem "rspec-rebound"
gem 'selenium-webdriver' gem 'selenium-webdriver'
gem 'test-queue'
gem 'timecop' gem 'timecop'
gem 'vcr' gem 'vcr'
end end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ class CropsController < ApplicationController
@crops = Crop.search('*', boost_by: %i(plantings_count harvests_count), @crops = Crop.search('*', boost_by: %i(plantings_count harvests_count),
limit: 100, limit: 100,
page: params[:page], page: params[:page],
load: (request.format.csv? || request.format.rss? ? { include: %i(parent scientific_names seeds harvests creator plantings) } : false)) load: false)
@num_requested_crops = requested_crops.size if current_member @num_requested_crops = requested_crops.size if current_member
@filename = filename @filename = filename
respond_with @crops respond_with @crops

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

24
config.rb Normal file
View File

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

View File

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

4
config/compass.rb Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +0,0 @@
class AddWikidataAndTempsToGardens < ActiveRecord::Migration[7.2]
def change
add_column :gardens, :location_wikidata_id, :string
add_column :gardens, :lowest_temp_c, :float
add_column :gardens, :highest_temp_c, :float
end
end

View File

@@ -1,13 +0,0 @@
# frozen_string_literal: true
class CreateBlocks < ActiveRecord::Migration[6.1]
def change
create_table :blocks do |t|
t.references :blocker, foreign_key: { to_table: :members }
t.references :blocked, foreign_key: { to_table: :members }
t.timestamps
end
add_index :blocks, %i(blocker_id blocked_id), unique: true
end
end

View File

@@ -37,6 +37,6 @@ class CreateVersions < ActiveRecord::Migration[7.2]
t.string :event, null: false t.string :event, null: false
t.text :object, limit: TEXT_BYTES t.text :object, limit: TEXT_BYTES
end end
add_index :versions, %i(item_type item_id) add_index :versions, %i[item_type item_id]
end end
end end

View File

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

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