mirror of
https://github.com/Growstuff/growstuff.git
synced 2026-05-25 01:13:03 -04:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dc5dbac7d | ||
|
|
ee6d5cd84a | ||
|
|
f1524c2b0c | ||
|
|
a6cb1cbb36 | ||
|
|
c0f6720a1e | ||
|
|
04ea628f00 | ||
|
|
cabac926cc | ||
|
|
0d375f6146 | ||
|
|
be9e5ed6bf | ||
|
|
c8ea225b10 | ||
|
|
a8b7c73111 | ||
|
|
61810dbee3 | ||
|
|
1467ec9364 | ||
|
|
37452a5513 | ||
|
|
e70297a83e | ||
|
|
d7a50f86b5 | ||
|
|
ca7f56683c | ||
|
|
0c00b866da | ||
|
|
f50da4e0e0 | ||
|
|
31285b2bde | ||
|
|
ec5873fc88 | ||
|
|
555d5ddf15 | ||
|
|
5ada7e7f77 | ||
|
|
4659ac5464 | ||
|
|
3d63d12908 | ||
|
|
035210197f | ||
|
|
8a8fd6eabd | ||
|
|
fe9fdd9147 | ||
|
|
ee7b9ab39f | ||
|
|
6aadb4d805 | ||
|
|
a42682a59e | ||
|
|
6294c54139 | ||
|
|
c168e8e4c9 | ||
|
|
6ac438a07f | ||
|
|
2380c662fe | ||
|
|
4589839c64 | ||
|
|
1f6f3c4dfd | ||
|
|
5a7f41537f | ||
|
|
1281795c97 | ||
|
|
c219d447cc | ||
|
|
1e3f86a349 | ||
|
|
680afe02cc | ||
|
|
914cfe99c8 | ||
|
|
4643fbd92e | ||
|
|
5ac709ffd1 | ||
|
|
9833801a42 | ||
|
|
4d1e8aede6 | ||
|
|
24f41350a9 | ||
|
|
503ba716bb | ||
|
|
e423e6ac79 | ||
|
|
e63089e03b | ||
|
|
6ce347af82 | ||
|
|
64af597dec | ||
|
|
7160f50ac1 | ||
|
|
e748da9a1f | ||
|
|
4ac0dcb05b | ||
|
|
60390fcc06 |
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
ko_fi: jennyscottthompson
|
||||
@@ -1,25 +1,31 @@
|
||||
# This configuration was generated by
|
||||
# `rubocop --auto-gen-config`
|
||||
# on 2026-04-25 16:44:38 UTC using RuboCop version 1.86.1.
|
||||
# on 2026-05-02 06:53:56 UTC using RuboCop version 1.86.1.
|
||||
# The point is for the user to remove these configuration records
|
||||
# one by one as the offenses are removed from the code base.
|
||||
# Note that changes in the inspected code, or installation of new
|
||||
# versions of RuboCop, may require this file to be generated again.
|
||||
|
||||
# Offense count: 19
|
||||
Capybara/NegationMatcherAfterVisit:
|
||||
# Offense count: 407
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Capybara/RSpec/HaveContent:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 21
|
||||
Capybara/RSpec/NegationMatcherAfterVisit:
|
||||
Exclude:
|
||||
- 'spec/features/admin/reverting_crops_spec.rb'
|
||||
- 'spec/features/crops/crop_detail_page_spec.rb'
|
||||
- 'spec/features/crops/crop_wranglers_spec.rb'
|
||||
- 'spec/features/gardens/gardens_spec.rb'
|
||||
- 'spec/features/members/blocking_spec.rb'
|
||||
- 'spec/features/members/deletion_spec.rb'
|
||||
- 'spec/features/members/following_spec.rb'
|
||||
- 'spec/features/members/profile_spec.rb'
|
||||
- 'spec/features/plantings/planting_a_crop_spec.rb'
|
||||
|
||||
# Offense count: 14
|
||||
Capybara/SpecificMatcher:
|
||||
Capybara/RSpec/SpecificMatcher:
|
||||
Exclude:
|
||||
- 'spec/features/footer_spec.rb'
|
||||
- 'spec/features/gardens/adding_gardens_spec.rb'
|
||||
@@ -28,7 +34,7 @@ Capybara/SpecificMatcher:
|
||||
- 'spec/features/seeds/adding_seeds_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Capybara/VisibilityMatcher:
|
||||
Capybara/RSpec/VisibilityMatcher:
|
||||
Exclude:
|
||||
- 'spec/features/shared_examples/crop_suggest.rb'
|
||||
|
||||
@@ -63,7 +69,13 @@ FactoryBot/ExcessiveCreateList:
|
||||
- 'spec/features/crops/show_spec.rb'
|
||||
- 'spec/features/percy/percy_spec.rb'
|
||||
|
||||
# Offense count: 312
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Layout/EmptyLines:
|
||||
Exclude:
|
||||
- 'config/environments/production.rb'
|
||||
|
||||
# Offense count: 311
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowMultipleStyles, EnforcedHashRocketStyle, EnforcedColonStyle, EnforcedLastArgumentHashStyle.
|
||||
# SupportedHashRocketStyles: key, separator, table
|
||||
@@ -81,7 +93,7 @@ Layout/HashAlignment:
|
||||
- 'spec/requests/api/v1/activities_request_spec.rb'
|
||||
- 'spec/requests/api/v1/members_request_spec.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# Offense count: 8
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
|
||||
# URISchemes: http, https
|
||||
@@ -89,9 +101,20 @@ Layout/LineLength:
|
||||
Exclude:
|
||||
- 'Gemfile'
|
||||
- 'app/controllers/admin/versions_controller.rb'
|
||||
- 'app/controllers/crops_controller.rb'
|
||||
- 'app/models/concerns/predict_planting.rb'
|
||||
- 'app/models/crop.rb'
|
||||
- 'db/seeds.rb'
|
||||
- 'lib/tasks/members.rake'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle, IndentationWidth.
|
||||
# SupportedStyles: aligned, indented, indented_relative_to_receiver
|
||||
Layout/MultilineMethodCallIndentation:
|
||||
Exclude:
|
||||
- 'app/models/activity.rb'
|
||||
- 'app/models/concerns/predict_harvest.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
@@ -99,23 +122,15 @@ Lint/AmbiguousOperatorPrecedence:
|
||||
Exclude:
|
||||
- 'app/controllers/activities_controller.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 3
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: RequireParenthesesForMethodChains.
|
||||
Lint/AmbiguousRange:
|
||||
Exclude:
|
||||
- 'app/models/concerns/search_activities.rb'
|
||||
- 'app/models/concerns/search_harvests.rb'
|
||||
- 'app/models/concerns/search_plantings.rb'
|
||||
- 'db/seeds.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: AllowSafeAssignment.
|
||||
Lint/AssignmentInCondition:
|
||||
Exclude:
|
||||
- 'app/helpers/crops_helper.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: AllowedMethods.
|
||||
# AllowedMethods: enums
|
||||
@@ -158,7 +173,7 @@ Lint/UselessConstantScoping:
|
||||
Exclude:
|
||||
- 'app/controllers/members_controller.rb'
|
||||
|
||||
# Offense count: 61
|
||||
# Offense count: 65
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns, CountRepeatedAttributes.
|
||||
Metrics/AbcSize:
|
||||
Max: 295
|
||||
@@ -180,12 +195,12 @@ Metrics/CollectionLiteralLength:
|
||||
Exclude:
|
||||
- 'lib/tasks/import.rake'
|
||||
|
||||
# Offense count: 10
|
||||
# Offense count: 11
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/CyclomaticComplexity:
|
||||
Max: 32
|
||||
|
||||
# Offense count: 82
|
||||
# Offense count: 83
|
||||
# Configuration parameters: CountComments, CountAsOne, AllowedMethods, AllowedPatterns.
|
||||
Metrics/MethodLength:
|
||||
Max: 296
|
||||
@@ -195,7 +210,7 @@ Metrics/MethodLength:
|
||||
Metrics/ModuleLength:
|
||||
Max: 144
|
||||
|
||||
# Offense count: 8
|
||||
# Offense count: 10
|
||||
# Configuration parameters: AllowedMethods, AllowedPatterns.
|
||||
Metrics/PerceivedComplexity:
|
||||
Max: 32
|
||||
@@ -209,6 +224,16 @@ Naming/PredicateMethod:
|
||||
- 'app/models/concerns/finishable.rb'
|
||||
- 'app/models/seed.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: NamePrefix, ForbiddenPrefixes, AllowedMethods, MethodDefinitionMacros, UseSorbetSigs.
|
||||
# NamePrefix: is_, has_, have_, does_
|
||||
# ForbiddenPrefixes: is_, has_, have_, does_
|
||||
# AllowedMethods: is_a?
|
||||
# MethodDefinitionMacros: define_method, define_singleton_method
|
||||
Naming/PredicatePrefix:
|
||||
Exclude:
|
||||
- 'app/models/member.rb'
|
||||
|
||||
# Offense count: 3
|
||||
RSpec/AnyInstance:
|
||||
Exclude:
|
||||
@@ -221,34 +246,54 @@ RSpec/BeEq:
|
||||
Exclude:
|
||||
- 'spec/requests/api/v1/activities_request_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Offense count: 2
|
||||
RSpec/BeforeAfterAll:
|
||||
Exclude:
|
||||
- 'spec/tasks/import_spec.rb'
|
||||
- 'spec/tasks/members_spec.rb'
|
||||
|
||||
# Offense count: 298
|
||||
# Offense count: 311
|
||||
# Configuration parameters: Prefixes, AllowedPatterns.
|
||||
# Prefixes: when, with, without
|
||||
RSpec/ContextWording:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 36
|
||||
# Offense count: 2
|
||||
# Configuration parameters: IgnoredMetadata.
|
||||
RSpec/DescribeClass:
|
||||
Exclude:
|
||||
- 'spec/models/harvest_prediction_spec.rb'
|
||||
- 'spec/tasks/members_spec.rb'
|
||||
|
||||
# Offense count: 37
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
|
||||
# SupportedStyles: described_class, explicit
|
||||
RSpec/DescribedClass:
|
||||
Exclude:
|
||||
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
|
||||
- 'spec/models/like_spec.rb'
|
||||
- 'spec/models/member_spec.rb'
|
||||
- 'spec/services/timeline_service_spec.rb'
|
||||
|
||||
# Offense count: 146
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: AllowConsecutiveOneLiners.
|
||||
RSpec/EmptyLineAfterExample:
|
||||
Exclude:
|
||||
- 'spec/controllers/crops_controller_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
RSpec/EmptyLineAfterFinalLet:
|
||||
Exclude:
|
||||
- 'spec/controllers/crops_controller_spec.rb'
|
||||
|
||||
# Offense count: 161
|
||||
# Configuration parameters: CountAsOne.
|
||||
RSpec/ExampleLength:
|
||||
Max: 27
|
||||
|
||||
# Offense count: 32
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
@@ -283,27 +328,18 @@ RSpec/IncludeExamples:
|
||||
- 'spec/views/photos/show.html.haml_spec.rb'
|
||||
- 'spec/views/seeds/index.rss.haml_spec.rb'
|
||||
|
||||
# Offense count: 37
|
||||
# Offense count: 2
|
||||
# Configuration parameters: Max, AllowedIdentifiers, AllowedPatterns.
|
||||
RSpec/IndexedLet:
|
||||
Exclude:
|
||||
- 'spec/controllers/harvests_controller_spec.rb'
|
||||
- 'spec/controllers/plantings_controller_spec.rb'
|
||||
- 'spec/features/crops/crop_photos_spec.rb'
|
||||
- 'spec/features/members/list_spec.rb'
|
||||
- 'spec/features/members/profile_spec.rb'
|
||||
- 'spec/features/percy/percy_spec.rb'
|
||||
- 'spec/features/planting_reminder_spec.rb'
|
||||
- 'spec/features/timeline/index_spec.rb'
|
||||
- 'spec/models/member_spec.rb'
|
||||
- 'spec/views/forums/index.html.haml_spec.rb'
|
||||
- 'spec/models/activity_spec.rb'
|
||||
|
||||
# Offense count: 719
|
||||
# Offense count: 711
|
||||
# Configuration parameters: AssignmentOnly.
|
||||
RSpec/InstanceVariable:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 41
|
||||
# Offense count: 43
|
||||
RSpec/LetSetup:
|
||||
Enabled: false
|
||||
|
||||
@@ -318,7 +354,7 @@ RSpec/MessageChain:
|
||||
Exclude:
|
||||
- 'spec/models/member_spec.rb'
|
||||
|
||||
# Offense count: 23
|
||||
# Offense count: 65
|
||||
# Configuration parameters: .
|
||||
# SupportedStyles: have_received, receive
|
||||
RSpec/MessageSpies:
|
||||
@@ -329,11 +365,11 @@ RSpec/MultipleDescribes:
|
||||
Exclude:
|
||||
- 'spec/features/crops/crop_wranglers_spec.rb'
|
||||
|
||||
# Offense count: 191
|
||||
# Offense count: 235
|
||||
RSpec/MultipleExpectations:
|
||||
Max: 19
|
||||
|
||||
# Offense count: 166
|
||||
# Offense count: 171
|
||||
# Configuration parameters: AllowSubject.
|
||||
RSpec/MultipleMemoizedHelpers:
|
||||
Max: 16
|
||||
@@ -344,24 +380,35 @@ RSpec/MultipleMemoizedHelpers:
|
||||
RSpec/NamedSubject:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 109
|
||||
# Offense count: 112
|
||||
# Configuration parameters: AllowedGroups.
|
||||
RSpec/NestedGroups:
|
||||
Max: 6
|
||||
|
||||
# Offense count: 407
|
||||
# Offense count: 366
|
||||
# Configuration parameters: AllowedPatterns.
|
||||
# AllowedPatterns: ^expect_, ^assert_
|
||||
RSpec/NoExpectationExample:
|
||||
Enabled: false
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 9
|
||||
RSpec/PendingWithoutReason:
|
||||
Exclude:
|
||||
- 'spec/features/members/blocking_spec.rb'
|
||||
- 'spec/features/plantings/planting_a_crop_spec.rb'
|
||||
- 'spec/features/seeds/misc_seeds_spec.rb'
|
||||
- 'spec/features/unsubscribing_spec.rb'
|
||||
- 'spec/models/ability_spec.rb'
|
||||
- 'spec/requests/api/v1/gardens_request_spec.rb'
|
||||
|
||||
# Offense count: 5
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: Strict, EnforcedStyle, AllowedExplicitMatchers.
|
||||
# SupportedStyles: inflected, explicit
|
||||
RSpec/PredicateMatcher:
|
||||
Exclude:
|
||||
- 'spec/tasks/members_spec.rb'
|
||||
|
||||
# Offense count: 2
|
||||
RSpec/RepeatedDescription:
|
||||
Exclude:
|
||||
@@ -386,18 +433,18 @@ RSpec/ScatteredSetup:
|
||||
- 'spec/features/percy/percy_spec.rb'
|
||||
- 'spec/features/plantings/prediction_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Offense count: 2
|
||||
# Configuration parameters: CustomTransform, IgnoreMethods, IgnoreMetadata, InflectorPath, EnforcedInflector.
|
||||
# SupportedInflectors: default, active_support
|
||||
RSpec/SpecFilePathFormat:
|
||||
Exclude:
|
||||
- 'spec/controllers/member_controller_spec.rb'
|
||||
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# Offense count: 2
|
||||
RSpec/StubbedMock:
|
||||
Exclude:
|
||||
- 'spec/controllers/garden_types_controller_spec.rb'
|
||||
- 'spec/controllers/gardens_controller_spec.rb'
|
||||
- 'spec/controllers/photos_controller_spec.rb'
|
||||
- 'spec/models/member_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
@@ -414,6 +461,13 @@ RSpec/VerifiedDoubles:
|
||||
- 'spec/controllers/gardens_controller_spec.rb'
|
||||
- 'spec/views/devise/shared/_links_spec.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: Inferences.
|
||||
RSpecRails/InferredSpecType:
|
||||
Exclude:
|
||||
- 'spec/mailers/harvest_reminder_mailer_spec.rb'
|
||||
|
||||
# Offense count: 30
|
||||
# Configuration parameters: Database.
|
||||
# SupportedDatabases: mysql, postgresql
|
||||
@@ -470,11 +524,25 @@ Rails/HasManyOrHasOneDependent:
|
||||
- 'app/models/crop.rb'
|
||||
- 'app/models/member.rb'
|
||||
|
||||
# Offense count: 7
|
||||
Rails/HelperInstanceVariable:
|
||||
Exclude:
|
||||
- 'app/helpers/crops_helper.rb'
|
||||
- 'app/helpers/plantings_helper.rb'
|
||||
|
||||
# Offense count: 1
|
||||
Rails/I18nLocaleAssignment:
|
||||
Exclude:
|
||||
- 'spec/features/locale_spec.rb'
|
||||
|
||||
# Offense count: 5
|
||||
Rails/I18nLocaleTexts:
|
||||
Exclude:
|
||||
- 'app/controllers/blocks_controller.rb'
|
||||
- 'app/controllers/comments_controller.rb'
|
||||
- 'app/controllers/messages_controller.rb'
|
||||
- 'config/initializers/comfortable_mexican_sofa.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Configuration parameters: IgnoreScopes.
|
||||
Rails/InverseOf:
|
||||
@@ -488,6 +556,12 @@ Rails/LexicallyScopedActionFilter:
|
||||
- 'app/controllers/data_controller.rb'
|
||||
- 'app/controllers/registrations_controller.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Rails/OrderArguments:
|
||||
Exclude:
|
||||
- 'app/models/activity.rb'
|
||||
|
||||
# Offense count: 2
|
||||
Rails/OutputSafety:
|
||||
Exclude:
|
||||
@@ -614,7 +688,7 @@ Rake/MethodDefinitionInTask:
|
||||
Exclude:
|
||||
- 'lib/tasks/growstuff.rake'
|
||||
|
||||
# Offense count: 4
|
||||
# Offense count: 5
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle, EnforcedStyleForClasses, EnforcedStyleForModules.
|
||||
# SupportedStyles: nested, compact
|
||||
@@ -623,10 +697,17 @@ Rake/MethodDefinitionInTask:
|
||||
Style/ClassAndModuleChildren:
|
||||
Exclude:
|
||||
- 'app/controllers/admin/crops_controller.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/actions/oauth_signup_action.rb'
|
||||
- 'lib/haml/filters/escaped_markdown.rb'
|
||||
- 'lib/haml/filters/growstuff_markdown.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/CollectionQuerying:
|
||||
Exclude:
|
||||
- 'app/models/member.rb'
|
||||
|
||||
# Offense count: 6
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/CommentedKeyword:
|
||||
@@ -657,12 +738,13 @@ Style/FloatDivision:
|
||||
Exclude:
|
||||
- 'app/models/concerns/predict_planting.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# Offense count: 2
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: EnforcedStyle.
|
||||
# SupportedStyles: always, always_true, never
|
||||
Style/FrozenStringLiteralComment:
|
||||
Exclude:
|
||||
- 'db/migrate/20260429132911_add_send_harvest_reminder_to_members.rb'
|
||||
- 'spec/lib/haml/filters/growstuff_markdown_spec.rb'
|
||||
|
||||
# Offense count: 2
|
||||
@@ -678,11 +760,14 @@ Style/IdenticalConditionalBranches:
|
||||
Exclude:
|
||||
- 'lib/actions/oauth_signup_action.rb'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
Style/MapIntoArray:
|
||||
# Offense count: 4
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Style/IfUnlessModifier:
|
||||
Exclude:
|
||||
- 'app/helpers/crops_helper.rb'
|
||||
- 'app/models/concerns/predict_planting.rb'
|
||||
- 'config/initializers/rack_attack.rb'
|
||||
- 'lib/tasks/growstuff.rake'
|
||||
|
||||
# Offense count: 1
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
@@ -736,6 +821,13 @@ Style/RedundantArgument:
|
||||
Exclude:
|
||||
- 'app/helpers/application_helper.rb'
|
||||
|
||||
# Offense count: 3
|
||||
# This cop supports safe autocorrection (--autocorrect).
|
||||
Style/RedundantBegin:
|
||||
Exclude:
|
||||
- 'app/models/concerns/predict_harvest.rb'
|
||||
- 'app/models/harvest.rb'
|
||||
|
||||
# Offense count: 4
|
||||
# This cop supports unsafe autocorrection (--autocorrect-all).
|
||||
# Configuration parameters: SafeForConstants.
|
||||
|
||||
@@ -1 +1 @@
|
||||
3.4.8
|
||||
3.4.9
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ruby:3.4.8-trixie
|
||||
FROM ruby:3.4.9-trixie
|
||||
|
||||
# Install system dependencies
|
||||
RUN apt-get update -qq && \
|
||||
|
||||
62
Gemfile.lock
62
Gemfile.lock
@@ -122,8 +122,8 @@ GEM
|
||||
autoprefixer-rails (10.4.16.0)
|
||||
execjs (~> 2)
|
||||
aws-eventstream (1.4.0)
|
||||
aws-partitions (1.1240.0)
|
||||
aws-sdk-core (3.245.0)
|
||||
aws-partitions (1.1252.0)
|
||||
aws-sdk-core (3.248.0)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.992.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
@@ -131,24 +131,24 @@ GEM
|
||||
bigdecimal
|
||||
jmespath (~> 1, >= 1.6.1)
|
||||
logger
|
||||
aws-sdk-kms (1.123.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-kms (1.128.0)
|
||||
aws-sdk-core (~> 3, >= 3.248.0)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sdk-s3 (1.220.0)
|
||||
aws-sdk-core (~> 3, >= 3.244.0)
|
||||
aws-sdk-s3 (1.224.0)
|
||||
aws-sdk-core (~> 3, >= 3.248.0)
|
||||
aws-sdk-kms (~> 1)
|
||||
aws-sigv4 (~> 1.5)
|
||||
aws-sigv4 (1.12.1)
|
||||
aws-eventstream (~> 1, >= 1.0.2)
|
||||
axe-core-api (4.11.2)
|
||||
axe-core-api (4.11.3)
|
||||
dumb_delegator
|
||||
ostruct
|
||||
virtus
|
||||
axe-core-capybara (4.11.2)
|
||||
axe-core-api (= 4.11.2)
|
||||
axe-core-capybara (4.11.3)
|
||||
axe-core-api (= 4.11.3)
|
||||
dumb_delegator
|
||||
axe-core-rspec (4.11.2)
|
||||
axe-core-api (= 4.11.2)
|
||||
axe-core-rspec (4.11.3)
|
||||
axe-core-api (= 4.11.3)
|
||||
dumb_delegator
|
||||
ostruct
|
||||
virtus
|
||||
@@ -236,7 +236,7 @@ GEM
|
||||
csv_shaper (1.4.0)
|
||||
activesupport (>= 3.0.0)
|
||||
csv
|
||||
dalli (5.0.2)
|
||||
dalli (5.0.4)
|
||||
logger
|
||||
database_cleaner (2.1.0)
|
||||
database_cleaner-active_record (>= 2, < 3)
|
||||
@@ -247,7 +247,7 @@ GEM
|
||||
date (3.5.1)
|
||||
descendants_tracker (0.0.4)
|
||||
thread_safe (~> 0.3, >= 0.3.1)
|
||||
devise (5.0.3)
|
||||
devise (5.0.4)
|
||||
bcrypt (~> 3.0)
|
||||
orm_adapter (~> 0.1)
|
||||
railties (>= 7.0)
|
||||
@@ -276,7 +276,7 @@ GEM
|
||||
elasticsearch-transport (7.0.0)
|
||||
faraday
|
||||
multi_json
|
||||
erb (6.0.2)
|
||||
erb (6.0.4)
|
||||
erubi (1.13.1)
|
||||
execjs (2.10.0)
|
||||
factory_bot (6.5.5)
|
||||
@@ -286,7 +286,7 @@ GEM
|
||||
railties (>= 6.1.0)
|
||||
faker (3.8.0)
|
||||
i18n (>= 1.8.11, < 2)
|
||||
faraday (2.14.1)
|
||||
faraday (2.14.2)
|
||||
faraday-net_http (>= 2.0, < 3.5)
|
||||
json
|
||||
logger
|
||||
@@ -297,7 +297,7 @@ GEM
|
||||
flickraw (0.9.10)
|
||||
font-awesome-sass (5.15.1)
|
||||
sassc (>= 1.11)
|
||||
friendly_id (5.6.0)
|
||||
friendly_id (5.7.0)
|
||||
activerecord (>= 4.0.0)
|
||||
gbifrb (0.2.0)
|
||||
geocoder (1.8.6)
|
||||
@@ -355,7 +355,7 @@ GEM
|
||||
terminal-table (>= 1.5.1)
|
||||
i18n_data (1.1.0)
|
||||
simple_po_parser (~> 1.1)
|
||||
icalendar (2.12.2)
|
||||
icalendar (2.12.3)
|
||||
base64
|
||||
ice_cube (~> 0.16)
|
||||
logger
|
||||
@@ -366,7 +366,7 @@ GEM
|
||||
mini_magick (>= 4.9.5, < 5)
|
||||
ruby-vips (>= 2.0.17, < 3)
|
||||
io-console (0.8.2)
|
||||
irb (1.17.0)
|
||||
irb (1.18.0)
|
||||
pp (>= 0.6.0)
|
||||
prism (>= 1.3.0)
|
||||
rdoc (>= 4.0.0)
|
||||
@@ -376,7 +376,7 @@ GEM
|
||||
rails-dom-testing (>= 1, < 3)
|
||||
railties (>= 4.2.0)
|
||||
thor (>= 0.14, < 2.0)
|
||||
json (2.19.3)
|
||||
json (2.19.5)
|
||||
json-schema (6.2.0)
|
||||
addressable (~> 2.8)
|
||||
bigdecimal (>= 3.1, < 5)
|
||||
@@ -453,13 +453,13 @@ GEM
|
||||
net-protocol
|
||||
netrc (0.11.0)
|
||||
nio4r (2.7.5)
|
||||
nokogiri (1.19.2)
|
||||
nokogiri (1.19.3)
|
||||
mini_portile2 (~> 2.8.2)
|
||||
racc (~> 1.4)
|
||||
nokogiri (1.19.2-x86_64-linux-gnu)
|
||||
nokogiri (1.19.3-x86_64-linux-gnu)
|
||||
racc (~> 1.4)
|
||||
oauth (0.5.6)
|
||||
oj (3.17.0)
|
||||
oj (3.17.1)
|
||||
bigdecimal (>= 3.0)
|
||||
ostruct (>= 0.2)
|
||||
omniauth (1.9.2)
|
||||
@@ -477,7 +477,7 @@ GEM
|
||||
paper_trail (17.0.0)
|
||||
activerecord (>= 7.1)
|
||||
request_store (~> 1.4)
|
||||
parallel (2.0.1)
|
||||
parallel (2.1.0)
|
||||
parser (3.3.11.1)
|
||||
ast (~> 2.4.1)
|
||||
racc
|
||||
@@ -630,7 +630,7 @@ GEM
|
||||
rswag-ui (2.17.0)
|
||||
actionpack (>= 5.2, < 8.2)
|
||||
railties (>= 5.2, < 8.2)
|
||||
rubocop (1.86.1)
|
||||
rubocop (1.86.2)
|
||||
json (~> 2.3)
|
||||
language_server-protocol (~> 3.17.0.2)
|
||||
lint_roller (~> 1.1.0)
|
||||
@@ -644,13 +644,13 @@ GEM
|
||||
rubocop-ast (1.49.1)
|
||||
parser (>= 3.3.7.2)
|
||||
prism (~> 1.7)
|
||||
rubocop-capybara (2.22.1)
|
||||
rubocop-capybara (2.23.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop (~> 1.81)
|
||||
rubocop-factory_bot (2.28.0)
|
||||
lint_roller (~> 1.1)
|
||||
rubocop (~> 1.72, >= 1.72.1)
|
||||
rubocop-rails (2.34.3)
|
||||
rubocop-rails (2.35.2)
|
||||
activesupport (>= 4.2.0)
|
||||
lint_roller (~> 1.1)
|
||||
rack (>= 1.1)
|
||||
@@ -670,7 +670,7 @@ GEM
|
||||
ruby-units (4.1.0)
|
||||
ruby-vips (2.2.1)
|
||||
ffi (~> 1.12)
|
||||
rubyzip (3.2.2)
|
||||
rubyzip (3.3.0)
|
||||
sass (3.7.4)
|
||||
sass-listen (~> 4.0.0)
|
||||
sass-listen (4.0.0)
|
||||
@@ -690,7 +690,7 @@ GEM
|
||||
activemodel (>= 6.1)
|
||||
hashie
|
||||
securerandom (0.4.1)
|
||||
selenium-webdriver (4.43.0)
|
||||
selenium-webdriver (4.44.0)
|
||||
base64 (~> 0.2)
|
||||
logger (~> 1.4)
|
||||
rexml (~> 3.2, >= 3.2.5)
|
||||
@@ -725,7 +725,7 @@ GEM
|
||||
thread_safe (0.3.6)
|
||||
tilt (2.7.0)
|
||||
timecop (0.9.11)
|
||||
timeout (0.5.0)
|
||||
timeout (0.6.1)
|
||||
tsort (0.2.0)
|
||||
tzinfo (2.0.6)
|
||||
concurrent-ruby (~> 1.0)
|
||||
@@ -886,7 +886,7 @@ DEPENDENCIES
|
||||
xmlrpc
|
||||
|
||||
RUBY VERSION
|
||||
ruby 3.4.8p72
|
||||
ruby 3.4.9
|
||||
|
||||
BUNDLED WITH
|
||||
2.4.22
|
||||
|
||||
@@ -4,21 +4,16 @@ class ActivitiesController < DataController
|
||||
def index
|
||||
@show_all = params[:all] == '1'
|
||||
|
||||
where = {}
|
||||
where['active'] = true unless @show_all
|
||||
@activities = Activity.includes(:owner).order(created_at: :desc)
|
||||
@activities = @activities.active unless @show_all
|
||||
|
||||
if params[:member_slug].present?
|
||||
@owner = Member.find_by!(slug: params[:member_slug])
|
||||
where['owner_id'] = @owner.id
|
||||
@activities = @activities.where(owner_id: @owner.id)
|
||||
end
|
||||
|
||||
@activities = Activity.search(
|
||||
where:,
|
||||
page: params[:page],
|
||||
limit: 30,
|
||||
boost_by: [:created_at],
|
||||
load: false
|
||||
)
|
||||
@activities = @activities.paginate(page: params[:page], per_page: 30)
|
||||
|
||||
@filename = "Growstuff-#{specifics}Activities-#{Time.zone.now.to_fs(:number)}.csv"
|
||||
respond_with(@activities)
|
||||
end
|
||||
|
||||
@@ -3,6 +3,36 @@
|
||||
module Api
|
||||
module V1
|
||||
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
|
||||
|
||||
@@ -68,7 +68,7 @@ class ApplicationController < ActionController::Base
|
||||
# profile stuff
|
||||
:bio, :location, :latitude, :longitude,
|
||||
# email settings
|
||||
:show_email, :newsletter, :send_notification_email, :send_planting_reminder)
|
||||
:show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder)
|
||||
end
|
||||
|
||||
devise_parameter_sanitizer.permit(:account_update) do |member|
|
||||
@@ -80,7 +80,7 @@ class ApplicationController < ActionController::Base
|
||||
:bio, :location, :latitude, :longitude,
|
||||
:website_url, :instagram_handle, :facebook_handle, :bluesky_handle, :other_url,
|
||||
# email settings
|
||||
:show_email, :newsletter, :send_notification_email, :send_planting_reminder,
|
||||
:show_email, :newsletter, :send_notification_email, :send_planting_reminder, :send_harvest_reminder,
|
||||
# update password
|
||||
:current_password)
|
||||
end
|
||||
|
||||
@@ -13,7 +13,7 @@ class CropsController < ApplicationController
|
||||
@crops = Crop.search('*', boost_by: %i(plantings_count harvests_count),
|
||||
limit: 100,
|
||||
page: params[:page],
|
||||
load: false)
|
||||
load: (request.format.csv? || request.format.rss? ? { include: %i(parent scientific_names seeds harvests creator plantings) } : false))
|
||||
@num_requested_crops = requested_crops.size if current_member
|
||||
@filename = filename
|
||||
respond_with @crops
|
||||
|
||||
@@ -57,12 +57,23 @@ class GardensController < DataController
|
||||
redirect_to(member_gardens_path(@garden.owner))
|
||||
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
|
||||
|
||||
def garden_params
|
||||
params.require(:garden).permit(
|
||||
: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
|
||||
|
||||
@@ -23,7 +23,7 @@ class HarvestsController < DataController
|
||||
@harvests = Harvest.search('*', where:,
|
||||
limit: 100,
|
||||
page: params[:page],
|
||||
load: false,
|
||||
load: (request.format.csv? ? { include: %i(crop owner plant_part) } : false),
|
||||
boost_by: [:created_at])
|
||||
|
||||
@filename = csv_filename
|
||||
@@ -38,9 +38,9 @@ class HarvestsController < DataController
|
||||
end
|
||||
|
||||
def new
|
||||
@harvest = Harvest.new(harvested_at: Time.zone.today)
|
||||
@planting = Planting.find_by(slug: params[:planting_slug]) if params[:planting_slug]
|
||||
@crop = Crop.find_by(id: params[:crop_id])
|
||||
@harvest = Harvest.new(new_harvest_params.merge(harvested_at: Time.zone.today))
|
||||
@planting = @harvest.planting
|
||||
@crop = @harvest.crop
|
||||
respond_with(@harvest)
|
||||
end
|
||||
|
||||
@@ -52,7 +52,7 @@ class HarvestsController < DataController
|
||||
def create
|
||||
@harvest.crop_id = @harvest.planting.crop_id if @harvest.planting_id
|
||||
@harvest.harvested_at = Time.zone.now if @harvest.harvested_at.blank?
|
||||
@harvest.save
|
||||
update_planting_rating if @harvest.save
|
||||
if params[:return] == 'planting'
|
||||
respond_with(@harvest, location: @harvest.planting)
|
||||
else
|
||||
@@ -61,7 +61,7 @@ class HarvestsController < DataController
|
||||
end
|
||||
|
||||
def update
|
||||
@harvest.update(harvest_params)
|
||||
update_planting_rating if @harvest.update(harvest_params)
|
||||
respond_with(@harvest)
|
||||
end
|
||||
|
||||
@@ -76,7 +76,17 @@ class HarvestsController < DataController
|
||||
params.require(:harvest)
|
||||
.permit(:planting_id, :crop_id, :harvested_at, :description,
|
||||
:quantity, :unit, :weight_quantity, :weight_unit,
|
||||
:plant_part_id, :slug, :si_weight)
|
||||
:plant_part_id, :slug, :si_weight, :overall_rating)
|
||||
.merge(owner_id: current_member.id)
|
||||
end
|
||||
|
||||
def new_harvest_params
|
||||
return {} unless params[:harvest]
|
||||
|
||||
params.require(:harvest)
|
||||
.permit(:planting_id, :crop_id, :harvested_at, :description,
|
||||
:quantity, :unit, :weight_quantity, :weight_unit,
|
||||
:plant_part_id, :slug, :si_weight, :overall_rating)
|
||||
.merge(owner_id: current_member.id)
|
||||
end
|
||||
|
||||
@@ -103,4 +113,10 @@ class HarvestsController < DataController
|
||||
@harvest.planting.update_harvest_days!
|
||||
@harvest.crop.update_harvest_medians
|
||||
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
|
||||
|
||||
@@ -90,11 +90,12 @@ class MembersController < ApplicationController
|
||||
|
||||
EMAIL_TYPE_STRING = {
|
||||
send_notification_email: "direct message notifications",
|
||||
send_planting_reminder: "planting reminders"
|
||||
send_planting_reminder: "planting reminders",
|
||||
send_harvest_reminder: "harvest reminders"
|
||||
}.freeze
|
||||
|
||||
def member_params
|
||||
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter)
|
||||
params.require(:member).permit(:login_name, :tos_agreement, :email, :newsletter, :send_harvest_reminder)
|
||||
end
|
||||
|
||||
def member_json_fields
|
||||
|
||||
@@ -21,6 +21,10 @@ class PostsController < ApplicationController
|
||||
def new
|
||||
@post = Post.new
|
||||
@forum = Forum.find_by(id: params[:forum_id])
|
||||
if params[:crop_id]
|
||||
@crop = Crop.friendly.find(params[:crop_id])
|
||||
@post.body = "[#{@crop.name}](crop)"
|
||||
end
|
||||
respond_with(@post)
|
||||
end
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ class SeedsController < DataController
|
||||
page: params[:page],
|
||||
limit: 30,
|
||||
boost_by: [:created_at],
|
||||
load: false
|
||||
load: (request.format.csv? ? { include: %i(crop owner) } : false)
|
||||
)
|
||||
|
||||
respond_with(@seeds)
|
||||
|
||||
@@ -57,6 +57,19 @@ class NotifierMailer < ApplicationMailer
|
||||
mail(to: @member.email, subject: @subject) if @member.send_planting_reminder
|
||||
end
|
||||
|
||||
def harvest_reminder(member)
|
||||
@member = member
|
||||
@plantings = @member.plantings.active.select(&:harvest_in_next_week?)
|
||||
@sitename = ENV.fetch('GROWSTUFF_SITE_NAME', nil)
|
||||
@subject = I18n.t('notifier_mailer.harvest_reminder.subject', sitename: @sitename)
|
||||
|
||||
# Encrypting
|
||||
message = { member_id: @member.id, type: :send_harvest_reminder }
|
||||
@signed_message = verifier.generate(message)
|
||||
|
||||
mail(to: @member.email, subject: @subject) if @member.send_harvest_reminder
|
||||
end
|
||||
|
||||
def new_crop_request(member, request)
|
||||
@member = member
|
||||
@request = request
|
||||
|
||||
@@ -123,6 +123,7 @@ class Ability
|
||||
can :create, 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, member_id: member.id
|
||||
|
||||
can :create, Activity
|
||||
can :update, Activity, owner_id: member.id
|
||||
|
||||
@@ -4,7 +4,6 @@ class Activity < ApplicationRecord
|
||||
extend FriendlyId
|
||||
include Ownable
|
||||
include Finishable
|
||||
include SearchActivities
|
||||
include Likeable
|
||||
|
||||
belongs_to :garden, optional: true
|
||||
@@ -46,4 +45,17 @@ class Activity < ApplicationRecord
|
||||
def planting_slug
|
||||
planting&.crop&.slug
|
||||
end
|
||||
|
||||
scope :active, -> { where(finished: [false, nil]) }
|
||||
|
||||
def self.homepage_records(limit)
|
||||
# Get the latest activity for each owner, then return the latest 'limit' of those
|
||||
Activity.where(id: Activity.unscoped.select("DISTINCT ON (owner_id) id").order("owner_id, created_at DESC"))
|
||||
.order(created_at: :desc)
|
||||
.limit(limit)
|
||||
end
|
||||
|
||||
def self.reindex(refresh: false); end
|
||||
|
||||
def reindex(refresh: false); end
|
||||
end
|
||||
|
||||
@@ -60,10 +60,16 @@ module PredictHarvest
|
||||
def before_harvest_time?
|
||||
first_harvest_predicted_at.present? &&
|
||||
harvests.empty? &&
|
||||
first_harvest_predicted_at.present? &&
|
||||
first_harvest_predicted_at > Time.zone.today
|
||||
end
|
||||
|
||||
def harvest_in_next_week?
|
||||
first_harvest_predicted_at.present? &&
|
||||
harvests.empty? &&
|
||||
first_harvest_predicted_at >= Time.zone.today &&
|
||||
first_harvest_predicted_at <= Time.zone.today + 7.days
|
||||
end
|
||||
|
||||
def harvest_months
|
||||
Rails.cache.fetch("#{cache_key_with_version}/harvest_months", expires_in: 5.minutes) do
|
||||
neighbours_for_harvest_predictions.where.not(harvested_at: nil)
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
module SearchActivities
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
searchkick merge_mappings: true,
|
||||
settings: { number_of_shards: 1, number_of_replicas: 0 },
|
||||
mappings: {
|
||||
properties: {
|
||||
active: { type: :boolean },
|
||||
created_at: { type: :integer },
|
||||
updated_at: { type: :integer },
|
||||
due_date: { type: :date }
|
||||
}
|
||||
}
|
||||
|
||||
def search_data
|
||||
{
|
||||
slug:,
|
||||
active:,
|
||||
finished: finished?,
|
||||
name:,
|
||||
due_date:,
|
||||
category:,
|
||||
garden_id:,
|
||||
garden_name: garden&.name,
|
||||
garden_slug: garden&.garden_slug,
|
||||
planting_id:,
|
||||
planting_name: planting&.crop&.name,
|
||||
planting_slug: planting&.slug,
|
||||
description:,
|
||||
|
||||
# owner
|
||||
owner_id:,
|
||||
owner_login_name:,
|
||||
owner_slug:,
|
||||
|
||||
# timestamps
|
||||
created_at: created_at.to_i,
|
||||
updated_at: updated_at.to_i
|
||||
}
|
||||
end
|
||||
|
||||
def self.homepage_records(limit)
|
||||
records = []
|
||||
owners = []
|
||||
1..limit.times do
|
||||
where = {
|
||||
# photos_count: { gt: 0 },
|
||||
owner_id: { not: owners }
|
||||
}
|
||||
one_record = search('*',
|
||||
limit: 1,
|
||||
where:,
|
||||
boost_by: [:created_at],
|
||||
load: false).first
|
||||
return records if one_record.nil?
|
||||
|
||||
owners << one_record.owner_id
|
||||
records << one_record
|
||||
end
|
||||
records
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -165,9 +165,11 @@ class Crop < ApplicationRecord
|
||||
end
|
||||
|
||||
def all_companions
|
||||
return companions unless parent
|
||||
|
||||
(companions + parent.all_companions).uniq
|
||||
@all_companions ||= if parent
|
||||
(companions + parent.all_companions).uniq
|
||||
else
|
||||
companions
|
||||
end
|
||||
end
|
||||
|
||||
before_destroy :destroy_reverse_companionships
|
||||
|
||||
@@ -21,6 +21,7 @@ class Garden < ApplicationRecord
|
||||
after_validation :cleanup_area
|
||||
after_validation :geocode
|
||||
after_validation :empty_unwanted_geocodes
|
||||
after_validation :populate_wikidata_info, if: :will_save_change_to_location?
|
||||
after_save :mark_inactive_garden_plantings_as_finished
|
||||
|
||||
scope :active, -> { where(active: true) }
|
||||
@@ -92,6 +93,19 @@ class Garden < ApplicationRecord
|
||||
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
|
||||
|
||||
def strip_blanks
|
||||
|
||||
@@ -8,6 +8,8 @@ class Harvest < ApplicationRecord
|
||||
include SearchHarvests
|
||||
include Likeable
|
||||
|
||||
attr_accessor :overall_rating
|
||||
|
||||
friendly_id :harvest_slug, use: %i(slugged finders)
|
||||
|
||||
# Constants
|
||||
|
||||
@@ -79,6 +79,7 @@ class Member < ApplicationRecord
|
||||
scope :interesting, -> { confirmed.located.recently_signed_in.has_plantings }
|
||||
scope :has_plantings, -> { joins(:plantings).group("members.id") }
|
||||
scope :wants_reminders, -> { where(send_planting_reminder: true) }
|
||||
scope :wants_harvest_reminders, -> { where(send_harvest_reminder: true) }
|
||||
|
||||
# Include default devise modules. Others available are:
|
||||
# :token_authenticatable, :confirmable,
|
||||
@@ -161,7 +162,7 @@ class Member < ApplicationRecord
|
||||
end
|
||||
|
||||
def unread_count
|
||||
receipts.where(is_read: false).count
|
||||
@unread_count ||= receipts.where(is_read: false).count
|
||||
end
|
||||
|
||||
def self.login_name_or_email(login)
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
class CropSearchService
|
||||
# Crop.search(string)
|
||||
def self.search(query, page: 1, per_page: 12, current_member: nil)
|
||||
def self.search(query, page: 1, per_page: 12, current_member: nil, **options)
|
||||
search_params = {
|
||||
page:,
|
||||
per_page:,
|
||||
@@ -12,7 +12,7 @@ class CropSearchService
|
||||
includes: %i(scientific_names alternate_names),
|
||||
misspellings: { edit_distance: 2 },
|
||||
load: false
|
||||
}
|
||||
}.merge(options)
|
||||
# prioritise crops the member has planted
|
||||
search_params[:boost_where] = { planters_ids: current_member.id } if current_member
|
||||
|
||||
|
||||
74
app/services/wikidata_service.rb
Normal file
74
app/services/wikidata_service.rb
Normal file
@@ -0,0 +1,74 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'net/http'
|
||||
require 'json'
|
||||
|
||||
class WikidataService
|
||||
CELSIUS_UNIT_ID = 'http://www.wikidata.org/entity/Q25267'
|
||||
FAHRENHEIT_UNIT_ID = 'http://www.wikidata.org/entity/Q42289'
|
||||
|
||||
def self.find_wikidata_id(location_name)
|
||||
return nil if location_name.blank?
|
||||
|
||||
uri = URI("https://www.wikidata.org/w/api.php?action=wbsearchentities&search=#{URI.encode_www_form_component(location_name)}&language=en&format=json")
|
||||
req = Net::HTTP::Get.new(uri)
|
||||
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
|
||||
|
||||
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
||||
http.request(req)
|
||||
end
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
data.dig('search', 0, 'id')
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "WikidataService.find_wikidata_id error: #{e.message}"
|
||||
nil
|
||||
end
|
||||
|
||||
def self.fetch_temps(wikidata_id)
|
||||
return {} if wikidata_id.blank?
|
||||
|
||||
uri = URI("https://www.wikidata.org/w/api.php?action=wbgetentities&ids=#{wikidata_id}&props=claims&format=json")
|
||||
req = Net::HTTP::Get.new(uri)
|
||||
req['User-Agent'] = "Growstuff (https://www.growstuff.org; admin@growstuff.org)"
|
||||
|
||||
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
|
||||
http.request(req)
|
||||
end
|
||||
|
||||
data = JSON.parse(response.body)
|
||||
claims = data.dig('entities', wikidata_id, 'claims') || {}
|
||||
|
||||
highest_temp = extract_temp(claims['P6591'])
|
||||
lowest_temp = extract_temp(claims['P7422'])
|
||||
|
||||
{
|
||||
highest_temp_c: highest_temp,
|
||||
lowest_temp_c: lowest_temp
|
||||
}
|
||||
rescue StandardError => e
|
||||
Rails.logger.error "WikidataService.fetch_temps error: #{e.message}"
|
||||
{}
|
||||
end
|
||||
|
||||
def self.extract_temp(claim_data)
|
||||
return nil if claim_data.blank?
|
||||
|
||||
# We take the first value
|
||||
main_snak = claim_data.first&.dig('mainsnak')
|
||||
return nil unless main_snak&.dig('datavalue', 'type') == 'quantity'
|
||||
|
||||
quantity_data = main_snak.dig('datavalue', 'value')
|
||||
amount = quantity_data['amount'].to_f
|
||||
unit = quantity_data['unit']
|
||||
|
||||
case unit
|
||||
when CELSIUS_UNIT_ID
|
||||
amount
|
||||
when FAHRENHEIT_UNIT_ID
|
||||
(amount - 32) * 5.0 / 9.0
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -3,7 +3,7 @@
|
||||
- content_for :breadcrumbs do
|
||||
- if @owner
|
||||
%li.breadcrumb-item= link_to 'Activities', activities_path
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s activities", activities_path(@owner)
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s activities", member_activities_path(@owner)
|
||||
- else
|
||||
%li.breadcrumb-item.active= link_to 'Activities', activities_path
|
||||
|
||||
|
||||
@@ -5,5 +5,8 @@
|
||||
= render 'plantings/modal', planting: Planting.new(crop: crop, owner: current_member)
|
||||
= render 'harvests/modal', harvest: Harvest.new(crop: @crop, owner: current_member)
|
||||
= render 'seeds/modal', seed: Seed.new(crop: @crop, owner: current_member)
|
||||
= link_to new_post_path(crop_id: crop.slug), class: 'btn', id: 'post-button' do
|
||||
= post_icon
|
||||
Post
|
||||
- if active_plantings.any?
|
||||
= render 'plantings/failed_modal', crop: crop, active_plantings: active_plantings
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
%li.list-group-item= link_to "View all #{crop.name} harvests", crop_harvests_path(crop), class: 'card-link'
|
||||
- if crop.approved?
|
||||
- if current_member
|
||||
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(crop_id: crop.id), class: 'btn btn-block'
|
||||
%li.list-group-item= link_to "Harvest #{crop.name}", new_harvest_path(harvest: { crop_id: crop.id }), class: 'btn btn-block'
|
||||
- else
|
||||
%li.list-group-item.active
|
||||
= icon 'fas', 'user'
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
Nobody has posted about #{crop.name.pluralize} yet.
|
||||
%p
|
||||
- if can? :create, Post
|
||||
= link_to "Post something", new_post_path, class: 'btn btn-default'
|
||||
= link_to "Post something", new_post_path(crop_id: crop.slug), class: 'btn btn-default'
|
||||
- else
|
||||
= render partial: "shared/signin_signup",
|
||||
locals: { to: "post your tips and experiences growing #{crop.name.pluralize}" }
|
||||
|
||||
@@ -44,8 +44,10 @@ csv.headers *all_headers
|
||||
@crops.each do |c|
|
||||
csv.row c do |csv, crop|
|
||||
|
||||
csv.cells :id, :name, :en_wikipedia_url
|
||||
csv.cell :growstuff_url, crop_url(c)
|
||||
csv.cell :id, c.id
|
||||
csv.cell :name, c.name
|
||||
csv.cell :en_wikipedia_url, c.en_wikipedia_url
|
||||
csv.cell :growstuff_url, crop_url(slug: c.slug)
|
||||
|
||||
if c.scientific_names.any?
|
||||
csv.cell :default_scientific_name, c.default_scientific_name
|
||||
@@ -58,10 +60,10 @@ csv.headers *all_headers
|
||||
end
|
||||
|
||||
csv.cell :plantings_count, c.plantings_count || 0
|
||||
csv.cell :seeds_count, c.seeds.size[]
|
||||
csv.cell :seeds_count, c.seeds.size
|
||||
csv.cell :harvests_count, c.harvests.size
|
||||
|
||||
# Sunniness
|
||||
# Sunniness
|
||||
|
||||
sunniness = c.sunniness
|
||||
sunniness_rec = sunniness.max_by{|k,v| v}
|
||||
@@ -74,7 +76,7 @@ csv.headers *all_headers
|
||||
|
||||
# Planted from
|
||||
|
||||
planted_from = c.planted_from
|
||||
planted_from = c.planted_from
|
||||
planted_from_rec = planted_from.max_by{|k,v| v}
|
||||
if planted_from_rec
|
||||
csv.cell :plant_from_recommendation, planted_from_rec[0]
|
||||
@@ -105,12 +107,11 @@ csv.headers *all_headers
|
||||
csv.cell col, harvested_plant_parts[pp] || 0
|
||||
end
|
||||
|
||||
csv.cell :added_by_member_id, c.creator.id
|
||||
csv.cell :added_by_member_name, c.creator.to_s
|
||||
csv.cell :added_by_member_id, c.creator&.id
|
||||
csv.cell :added_by_member_name, c.creator&.to_s
|
||||
csv.cell :date_added, c.created_at.to_fs(:db)
|
||||
csv.cell :last_modified, c.updated_at.to_fs(:db)
|
||||
|
||||
csv.cell :license, "CC-BY-SA Growstuff http://growstuff.org/"
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
@@ -36,10 +36,11 @@
|
||||
= cute_icon
|
||||
= render 'predictions', crop: @crop
|
||||
- if @crop.all_companions.any?
|
||||
%section.companions
|
||||
%h2 Companions
|
||||
- @crop.all_companions.each do |companion|
|
||||
= render 'crops/tiny', crop: companion
|
||||
- cache [@crop, 'companions'] do
|
||||
%section.companions
|
||||
%h2 Companions
|
||||
- @crop.all_companions.each do |companion|
|
||||
= render 'crops/tiny', crop: companion
|
||||
|
||||
- if crop_or_parent(@crop, :en_youtube_url).present?
|
||||
%section.youtube
|
||||
|
||||
@@ -28,6 +28,12 @@
|
||||
= f.check_box :send_planting_reminder
|
||||
Receive regular reminders to track your planting and harvesting.
|
||||
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-8.checkbox
|
||||
%label
|
||||
= f.check_box :send_harvest_reminder
|
||||
Receive regular reminders of upcoming harvests.
|
||||
|
||||
.form-group
|
||||
.col-md-offset-2.col-md-8.checkbox
|
||||
%label
|
||||
|
||||
@@ -102,6 +102,8 @@
|
||||
%strong Collaborators:
|
||||
- if can?(:create, GardenCollaborator.new(garden: @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?
|
||||
%ul
|
||||
- @garden.garden_collaborators.each do |collabator|
|
||||
@@ -122,6 +124,31 @@
|
||||
%strong Garden type:
|
||||
= @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-header
|
||||
%h4 #{@garden.owner}'s gardens
|
||||
|
||||
@@ -52,6 +52,17 @@
|
||||
= f.select(:weight_unit, Harvest::WEIGHT_UNITS_VALUES, { include_blank: false }, class: 'form-control')
|
||||
= 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
|
||||
.text-right= f.submit 'Save'
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
.index-cards
|
||||
- harvest.crop.plant_parts.order(:name).each do |plant_part|
|
||||
.card
|
||||
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post do
|
||||
= link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}) do
|
||||
.card-title.text-center
|
||||
%h3= plant_part_icon(plant_part.name)
|
||||
%h3= plant_part.name
|
||||
@@ -22,10 +22,12 @@
|
||||
%h6 All Plant parts
|
||||
%nav.nav
|
||||
- PlantPart.all.order(:name).each do |plant_part|
|
||||
= link_to harvests_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), method: :post, class: 'nav-link border' do
|
||||
= link_to new_harvest_path(harvest: {planting_id: harvest.planting_id, crop_id: harvest.crop_id, plant_part_id: plant_part.id}), class: 'nav-link border' do
|
||||
= plant_part_icon(plant_part.name)
|
||||
= plant_part
|
||||
|
||||
|
||||
|
||||
%a.btn#modalHarvestButton{"data-bs-target" => "#modelHarvestForm", "data-bs-toggle" => "modal", href: ""}
|
||||
= harvest_icon
|
||||
Record harvest
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
- content_for :breadcrumbs do
|
||||
- if @owner
|
||||
%li.breadcrumb-item= link_to 'Harvests', harvests_path
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s harvests", harvests_path(@owner)
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s harvests", member_harvests_path(@owner)
|
||||
- else
|
||||
%li.breadcrumb-item.active= link_to "Harvests", harvests_path
|
||||
.row
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
%h2= t('.recently_planted')
|
||||
- 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
|
||||
.d-flex.w-100.justify-content-between.homepage--list-item
|
||||
%p.mb-2
|
||||
|
||||
33
app/views/notifier_mailer/harvest_reminder.html.haml
Normal file
33
app/views/notifier_mailer/harvest_reminder.html.haml
Normal file
@@ -0,0 +1,33 @@
|
||||
%p Hello #{@member.login_name},
|
||||
|
||||
%h2= t('notifier_mailer.harvest_reminder.heading')
|
||||
|
||||
%p= t('notifier_mailer.harvest_reminder.intro')
|
||||
|
||||
%ul
|
||||
- @plantings.each do |planting|
|
||||
%li
|
||||
= render 'planting', planting: planting
|
||||
(Predicted harvest date: #{planting.first_harvest_predicted_at.to_date})
|
||||
|
||||
%p
|
||||
Harvested anything lately?
|
||||
= link_to "Track your harvests here.", new_harvest_url, class: 'btn'
|
||||
|
||||
%p
|
||||
Track and predict your entire garden, and keep your garden records up to date at
|
||||
= link_to member_gardens_url(@member), class: 'btn' do
|
||||
your garden overview
|
||||
and on
|
||||
= link_to member_url(@member) do
|
||||
your profile page
|
||||
|
||||
%h4
|
||||
See you soon on #{@sitename}!
|
||||
|
||||
= render partial: 'signature'
|
||||
|
||||
%hr/
|
||||
%p
|
||||
Don't want to get these emails any more?
|
||||
= link_to t('notifier_mailer.harvest_reminder.unsubscribe'), unsubscribe_member_url(message: @signed_message)
|
||||
@@ -4,7 +4,7 @@
|
||||
- content_for :breadcrumbs do
|
||||
- if @owner
|
||||
%li.breadcrumb-item= link_to 'Plantings', plantings_path
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s plantings", plantings_path(@owner)
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s plantings", member_plantings_path(@owner)
|
||||
- else
|
||||
%li.breadcrumb-item.active= link_to 'Plantings', plantings_path
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ csv.headers :id,
|
||||
|
||||
csv.cell :quantity
|
||||
|
||||
csv.cell :plant_before, s.plant_before ? s.plant_before.to_s(:db) : ''
|
||||
csv.cell :plant_before, s.plant_before ? s.plant_before.to_fs(:db) : ''
|
||||
|
||||
csv.cell :tradable_to
|
||||
csv.cell :from_location, s.owner.location
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
- content_for :breadcrumbs do
|
||||
- if @owner
|
||||
%li.breadcrumb-item= link_to 'Seeds', seeds_path
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s seeds", seeds_path(@owner)
|
||||
%li.breadcrumb-item.active= link_to "#{@owner}'s seeds", member_seeds_path(@owner)
|
||||
- else
|
||||
%li.breadcrumb-item.active= link_to 'Seeds', seeds_path
|
||||
|
||||
|
||||
@@ -3,4 +3,4 @@
|
||||
%button.close{ type: "button", "data-bs-dismiss" => "alert" }
|
||||
%span{ "aria-hidden" => true } ×
|
||||
%span.sr-only Close
|
||||
= content
|
||||
= sanitize(content)
|
||||
|
||||
@@ -16,10 +16,6 @@ Rails.application.configure do
|
||||
config.consider_all_requests_local = false
|
||||
config.action_controller.perform_caching = true
|
||||
|
||||
# Attempt to read encrypted secrets from `config/secrets.yml.enc`.
|
||||
# Requires an encryption key in `ENV["RAILS_MASTER_KEY"]` or
|
||||
# `config/secrets.yml.key`.
|
||||
config.read_encrypted_secrets = true
|
||||
|
||||
# Disable serving static files from the `/public` folder by default since
|
||||
# Apache or NGINX already handles this.
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
# config/initializers/sidekiq.rb
|
||||
|
||||
Sidekiq.configure_server do |config|
|
||||
config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" }
|
||||
config.redis = { url: 'redis://localhost:6379/0' }
|
||||
end
|
||||
|
||||
Sidekiq.configure_client do |config|
|
||||
config.redis = { url: 'redis://localhost:6379/0', namespace: "app3_sidekiq_#{Rails.env}" }
|
||||
config.redis = { url: 'redis://localhost:6379/0' }
|
||||
end
|
||||
|
||||
@@ -456,3 +456,9 @@ en:
|
||||
all: Not authorized to %{action} %{subject}.
|
||||
read:
|
||||
notification: You must be signed in to view notifications.
|
||||
notifier_mailer:
|
||||
harvest_reminder:
|
||||
subject: "Upcoming harvests in your garden - %{sitename}"
|
||||
heading: "Upcoming harvests in your garden"
|
||||
intro: "Based on our predictions, the following plantings in your garden are likely to be ready for harvest within the next week:"
|
||||
unsubscribe: "Unsubscribe from harvest reminders"
|
||||
|
||||
@@ -32,6 +32,7 @@ Rails.application.routes.draw do
|
||||
|
||||
resources :gardens, concerns: :has_photos, param: :slug do
|
||||
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
|
||||
post 'fetch_wikidata' => 'gardens#fetch_wikidata', on: :member
|
||||
|
||||
resources :garden_collaborators
|
||||
end
|
||||
@@ -157,6 +158,7 @@ Rails.application.routes.draw do
|
||||
namespace :api do
|
||||
namespace :v1 do
|
||||
jsonapi_resources :activities
|
||||
get "crops/search", to: "crops#search"
|
||||
jsonapi_resources :crops
|
||||
jsonapi_resources :gardens
|
||||
jsonapi_resources :harvests
|
||||
|
||||
@@ -15,7 +15,5 @@ class AddActivities < ActiveRecord::Migration[7.1]
|
||||
end
|
||||
|
||||
add_column :members, :activities_count, :integer
|
||||
|
||||
Activity.reindex
|
||||
end
|
||||
end
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
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
|
||||
@@ -0,0 +1,5 @@
|
||||
class AddSendHarvestReminderToMembers < ActiveRecord::Migration[7.2]
|
||||
def change
|
||||
add_column :members, :send_harvest_reminder, :boolean, default: true, null: false
|
||||
end
|
||||
end
|
||||
@@ -10,7 +10,7 @@
|
||||
#
|
||||
# It's strongly recommended that you check this file into your version control system.
|
||||
|
||||
ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
|
||||
ActiveRecord::Schema[7.2].define(version: 2026_04_29_132911) do
|
||||
# These are extensions that must be enabled in order to support this database
|
||||
enable_extension "plpgsql"
|
||||
|
||||
@@ -631,6 +631,9 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
|
||||
t.decimal "area"
|
||||
t.string "area_unit"
|
||||
t.integer "garden_type_id"
|
||||
t.string "location_wikidata_id"
|
||||
t.float "lowest_temp_c"
|
||||
t.float "highest_temp_c"
|
||||
t.index ["garden_type_id"], name: "index_gardens_on_garden_type_id"
|
||||
t.index ["owner_id"], name: "index_gardens_on_owner_id"
|
||||
t.index ["slug"], name: "index_gardens_on_slug", unique: true
|
||||
@@ -786,6 +789,7 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
|
||||
t.string "facebook_handle"
|
||||
t.string "bluesky_handle"
|
||||
t.string "other_url"
|
||||
t.boolean "send_harvest_reminder", default: true, null: false
|
||||
t.index ["confirmation_token"], name: "index_members_on_confirmation_token", unique: true
|
||||
t.index ["discarded_at"], name: "index_members_on_discarded_at"
|
||||
t.index ["email"], name: "index_members_on_email", unique: true
|
||||
|
||||
@@ -48,8 +48,23 @@ namespace :growstuff do
|
||||
# Heroku scheduler only lets us run things daily, so this checks
|
||||
# Send on Monday
|
||||
if Time.zone.today.wday == 1
|
||||
Member.confirmed.wants_reminders.each do |m|
|
||||
Notifier.planting_reminder(m).deliver_now! unless m.plantings.active.empty?
|
||||
Member.confirmed.wants_reminders.find_each do |m|
|
||||
NotifierMailer.planting_reminder(m).deliver_later unless m.plantings.active.empty?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
desc "Send harvest reminder email"
|
||||
# usage: rake growstuff:send_harvest_reminders
|
||||
|
||||
task send_harvest_reminders: :environment do
|
||||
# Heroku scheduler only lets us run things daily, so this checks
|
||||
# Send on Wednesday
|
||||
if Time.zone.today.wday == 3
|
||||
Member.confirmed.wants_harvest_reminders.find_each do |m|
|
||||
if m.plantings.active.any?(&:harvest_in_next_week?)
|
||||
NotifierMailer.harvest_reminder(m).deliver_later
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -7,6 +7,5 @@ namespace :search do
|
||||
Planting.reindex
|
||||
Harvest.reindex
|
||||
Seed.reindex
|
||||
Activity.reindex
|
||||
end
|
||||
end
|
||||
|
||||
@@ -73,6 +73,21 @@ describe CropsController do
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET CSV" do
|
||||
let!(:tomato) { create(:tomato, en_wikipedia_url: "https://en.wikipedia.org/wiki/Tomato") }
|
||||
before do
|
||||
Crop.reindex
|
||||
get :index, format: "csv"
|
||||
end
|
||||
|
||||
it { is_expected.to be_successful }
|
||||
it { expect(response.content_type).to eq("text/csv; charset=utf-8") }
|
||||
it "contains tomato", pending: "not properly functional" do
|
||||
expect(assigns(:crops)).not_to be_empty
|
||||
expect(response.body).to include("tomato")
|
||||
end
|
||||
end
|
||||
|
||||
describe 'CREATE' do
|
||||
subject { put :create, params: crop_params }
|
||||
|
||||
|
||||
@@ -115,6 +115,21 @@ describe HarvestsController, :search do
|
||||
|
||||
it { expect(Harvest.last.planting.id).to eq(planting.id) }
|
||||
end
|
||||
|
||||
describe "updates planting rating" do
|
||||
let(:planting) { create(:planting, owner_id: member.id, garden: member.gardens.first) }
|
||||
|
||||
it "updates the planting rating when provided" do
|
||||
post :create, params: {
|
||||
harvest: valid_attributes.merge(
|
||||
planting_id: planting.id,
|
||||
crop_id: planting.crop_id,
|
||||
overall_rating: 4
|
||||
)
|
||||
}
|
||||
expect(planting.reload.overall_rating).to eq(4)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with invalid params" do
|
||||
@@ -171,6 +186,18 @@ describe HarvestsController, :search do
|
||||
|
||||
it { expect(response).to redirect_to(harvest) }
|
||||
end
|
||||
|
||||
describe "updates planting rating" do
|
||||
let(:planting) { create(:planting, owner_id: member.id, garden: member.gardens.first) }
|
||||
let(:harvest) do
|
||||
create(:harvest, valid_attributes.merge(planting_id: planting.id, crop_id: planting.crop_id))
|
||||
end
|
||||
|
||||
it "updates the planting rating when provided" do
|
||||
put :update, params: { slug: harvest.to_param, harvest: { overall_rating: 3 } }
|
||||
expect(planting.reload.overall_rating).to eq(3)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "with invalid params" do
|
||||
|
||||
@@ -10,6 +10,20 @@ describe PostsController do
|
||||
{ author_id: member.id, subject: "blah", body: "blah blah" }
|
||||
end
|
||||
|
||||
describe '#new' do
|
||||
let(:crop) { create(:crop, name: 'Bush Bean') }
|
||||
|
||||
it 'pre-populates the body when crop_id is present' do
|
||||
get :new, params: { crop_id: crop.slug }
|
||||
expect(assigns(:post).body).to eq("[#{crop.name}](crop)")
|
||||
end
|
||||
|
||||
it 'does not pre-populate the body when crop_id is absent' do
|
||||
get :new
|
||||
expect(assigns(:post).body).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
describe '#index' do
|
||||
before do
|
||||
create_list(:post, 100)
|
||||
|
||||
@@ -21,5 +21,9 @@ FactoryBot.define do
|
||||
description { "Stake tomato" }
|
||||
planting
|
||||
end
|
||||
|
||||
trait :reindex do
|
||||
# Activity is not using elasticsearch anymore, so we don't need to reindex
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -21,5 +21,9 @@ FactoryBot.define do
|
||||
factory :forum_post do
|
||||
forum
|
||||
end
|
||||
|
||||
trait :reindex do
|
||||
# Post is not using elasticsearch, but this trait is used in some tests
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -54,6 +54,8 @@ describe "Harvesting a crop", :js, :search do
|
||||
visit crop_path(maize)
|
||||
click_link "Record harvest"
|
||||
click_link plant_part.name
|
||||
# We then navigate to the new_harvest_path, and save.
|
||||
click_button "Save"
|
||||
end
|
||||
|
||||
it { expect(page).to have_content "harvest was successfully created." }
|
||||
@@ -69,9 +71,22 @@ describe "Harvesting a crop", :js, :search do
|
||||
click_link plant_part.name
|
||||
end
|
||||
|
||||
it { expect(page).to have_content "harvest was successfully created." }
|
||||
it { expect(page).to have_content planting.garden.name }
|
||||
it { expect(page).to have_content "maize" }
|
||||
it "saves" do
|
||||
# We then navigate to the new_harvest_path, and save.
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_content "harvest was successfully created."
|
||||
expect(page).to have_content planting.garden.name
|
||||
expect(page).to have_content "maize"
|
||||
end
|
||||
|
||||
it "updates the planting rating" do
|
||||
find_by_id('harvest_overall_rating').set 4
|
||||
click_button "Save"
|
||||
|
||||
expect(page).to have_content "harvest was successfully created."
|
||||
expect(planting.reload.overall_rating).to eq 4
|
||||
end
|
||||
end
|
||||
|
||||
context "Editing a harvest" do
|
||||
|
||||
28
spec/mailers/harvest_reminder_mailer_spec.rb
Normal file
28
spec/mailers/harvest_reminder_mailer_spec.rb
Normal file
@@ -0,0 +1,28 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe NotifierMailer, type: :mailer do
|
||||
let(:member) { create(:member) }
|
||||
let(:mail) { NotifierMailer.harvest_reminder(member) }
|
||||
|
||||
it "has a greeting" do
|
||||
expect(mail.body.encoded).to match "Hello"
|
||||
end
|
||||
|
||||
context "when member has upcoming harvests" do
|
||||
let(:crop) { create(:crop, median_days_to_first_harvest: 20) }
|
||||
let!(:planting) { create(:planting, owner: member, crop: crop, planted_at: 15.days.ago) }
|
||||
let(:plantings) { [planting] }
|
||||
|
||||
it "lists the upcoming harvest" do
|
||||
expect(mail.body.encoded).to match "Upcoming harvests in your garden"
|
||||
expect(mail.body.encoded).to match planting.crop.name
|
||||
expect(mail.body.encoded).to match (Time.zone.today + 5.days).to_date.to_s
|
||||
end
|
||||
|
||||
it "has an unsubscribe link" do
|
||||
expect(mail.body.encoded).to match "Unsubscribe from harvest reminders"
|
||||
end
|
||||
end
|
||||
end
|
||||
42
spec/models/activity_spec.rb
Normal file
42
spec/models/activity_spec.rb
Normal file
@@ -0,0 +1,42 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Activity do
|
||||
describe '.homepage_records' do
|
||||
let(:member1) { create(:member) }
|
||||
let(:member2) { create(:member) }
|
||||
|
||||
it 'returns the latest activities per owner' do
|
||||
create(:activity, owner: member1, created_at: 2.days.ago)
|
||||
latest_activity1 = create(:activity, owner: member1, created_at: 1.day.ago)
|
||||
latest_activity2 = create(:activity, owner: member2, created_at: Time.current)
|
||||
|
||||
records = described_class.homepage_records(10)
|
||||
expect(records).to contain_exactly(latest_activity1, latest_activity2)
|
||||
end
|
||||
|
||||
it 'respects the limit' do
|
||||
create(:activity, owner: member1)
|
||||
create(:activity, owner: member2)
|
||||
|
||||
records = described_class.homepage_records(1)
|
||||
expect(records.length).to eq(1)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'active scope' do
|
||||
it 'returns activities that are not finished' do
|
||||
active_activity = create(:activity, finished: false)
|
||||
finished_activity = create(:activity, finished: true)
|
||||
|
||||
expect(described_class.active).to include(active_activity)
|
||||
expect(described_class.active).not_to include(finished_activity)
|
||||
end
|
||||
|
||||
it 'treats nil finished as active' do
|
||||
activity = create(:activity, finished: nil)
|
||||
expect(described_class.active).to include(activity)
|
||||
end
|
||||
end
|
||||
end
|
||||
32
spec/models/garden_collaborator_ability_spec.rb
Normal file
32
spec/models/garden_collaborator_ability_spec.rb
Normal file
@@ -0,0 +1,32 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
require 'cancan/matchers'
|
||||
|
||||
describe Ability do
|
||||
let(:member) { create(:member) }
|
||||
let(:ability) { described_class.new(member) }
|
||||
|
||||
context 'garden collaborators' do
|
||||
let(:garden) { create(:garden) }
|
||||
let(:garden_collaborator) { create(:garden_collaborator, garden: garden, member: member) }
|
||||
let(:other_member) { create(:member) }
|
||||
let(:other_garden_collaborator) { create(:garden_collaborator, garden: garden, member: other_member) }
|
||||
|
||||
it 'can remove themselves as a collaborator' do
|
||||
expect(ability).to be_able_to(:destroy, garden_collaborator)
|
||||
end
|
||||
|
||||
it 'cannot remove others as a collaborator if not garden owner' do
|
||||
expect(ability).not_to be_able_to(:destroy, other_garden_collaborator)
|
||||
end
|
||||
|
||||
context 'as garden owner' do
|
||||
let(:garden) { create(:garden, owner: member) }
|
||||
|
||||
it 'can remove others as a collaborator' do
|
||||
expect(ability).to be_able_to(:destroy, other_garden_collaborator)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
41
spec/models/harvest_prediction_spec.rb
Normal file
41
spec/models/harvest_prediction_spec.rb
Normal file
@@ -0,0 +1,41 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
describe "Harvest prediction logic" do
|
||||
let(:crop) { create(:crop, median_days_to_first_harvest: 20) }
|
||||
let(:planting) { create(:planting, crop: crop) }
|
||||
|
||||
describe "#harvest_in_next_week?" do
|
||||
it "is true if predicted harvest is in 5 days" do
|
||||
planting.planted_at = 15.days.ago
|
||||
expect(planting.harvest_in_next_week?).to be true
|
||||
end
|
||||
|
||||
it "is true if predicted harvest is today" do
|
||||
planting.planted_at = 20.days.ago
|
||||
expect(planting.harvest_in_next_week?).to be true
|
||||
end
|
||||
|
||||
it "is true if predicted harvest is in 7 days" do
|
||||
planting.planted_at = 13.days.ago
|
||||
expect(planting.harvest_in_next_week?).to be true
|
||||
end
|
||||
|
||||
it "is false if predicted harvest is in 8 days" do
|
||||
planting.planted_at = 12.days.ago
|
||||
expect(planting.harvest_in_next_week?).to be false
|
||||
end
|
||||
|
||||
it "is false if predicted harvest was yesterday" do
|
||||
planting.planted_at = 21.days.ago
|
||||
expect(planting.harvest_in_next_week?).to be false
|
||||
end
|
||||
|
||||
it "is false if there are already harvests" do
|
||||
planting.planted_at = 15.days.ago
|
||||
create(:harvest, planting: planting, owner: planting.owner, crop: planting.crop, harvested_at: 1.day.ago)
|
||||
expect(planting.harvest_in_next_week?).to be false
|
||||
end
|
||||
end
|
||||
end
|
||||
35
spec/requests/api/v1/crops_search_spec.rb
Normal file
35
spec/requests/api/v1/crops_search_spec.rb
Normal file
@@ -0,0 +1,35 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe 'Crops Search' do
|
||||
subject { JSON.parse response.body }
|
||||
|
||||
let(:headers) { { 'Accept' => 'application/vnd.api+json' } }
|
||||
let!(:cabbage) { create(:crop, name: 'Cabbage', approval_status: 'approved') }
|
||||
let!(:apple) { create(:crop, name: 'Apple', approval_status: 'approved') }
|
||||
|
||||
describe 'GET /api/v1/crops/search' do
|
||||
before do
|
||||
Crop.reindex
|
||||
end
|
||||
|
||||
it 'returns crops matching the search term' do
|
||||
get '/api/v1/crops/search', params: { term: 'Cabbage' }, headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(subject['data'].size).to eq(1)
|
||||
expect(subject['data'].first['attributes']['name']).to eq('Cabbage')
|
||||
end
|
||||
|
||||
it 'returns empty data if no crops match' do
|
||||
get '/api/v1/crops/search', params: { term: 'NonExistent' }, headers: headers
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(subject['data']).to be_empty
|
||||
end
|
||||
|
||||
it 'includes meta information' do
|
||||
get '/api/v1/crops/search', params: { term: 'Cabbage' }, headers: headers
|
||||
expect(subject['meta']).to include('record_count' => 1, 'page_count' => 1)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -50,7 +50,6 @@ RSpec.configure do |config|
|
||||
Photo.reindex
|
||||
Planting.reindex
|
||||
Seed.reindex
|
||||
Activity.reindex
|
||||
end
|
||||
|
||||
config.before(:suite) do
|
||||
|
||||
@@ -90,13 +90,19 @@ describe 'layouts/_header.html.haml', type: "view" do
|
||||
expect(rendered).to have_content 'Inbox'
|
||||
expect(rendered).not_to match(/Inbox \d+/)
|
||||
end
|
||||
end
|
||||
|
||||
context 'has notifications' do
|
||||
it 'shows inbox count' do
|
||||
create(:notification, recipient: @member)
|
||||
render
|
||||
expect(rendered).to have_content 'Inbox 1'
|
||||
end
|
||||
context 'logged in, has notifications' do
|
||||
before do
|
||||
@member = create(:member)
|
||||
create(:notification, recipient: @member)
|
||||
sign_in @member
|
||||
controller.stub(:current_user) { @member }
|
||||
end
|
||||
|
||||
it 'shows inbox count' do
|
||||
render
|
||||
rendered.should have_content 'Inbox 1'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -9,6 +9,85 @@
|
||||
"/crops": {
|
||||
"get": {
|
||||
"summary": "crops List",
|
||||
"/crops/search": {
|
||||
"get": {
|
||||
"summary": "crops Search",
|
||||
"tags": [
|
||||
"crops"
|
||||
],
|
||||
"produces": [
|
||||
"application/vnd.api+json"
|
||||
],
|
||||
"parameters": [
|
||||
{
|
||||
"name": "term",
|
||||
"in": "query",
|
||||
"type": "string",
|
||||
"description": "Search term",
|
||||
"required": true
|
||||
},
|
||||
{
|
||||
"name": "page[number]",
|
||||
"in": "query",
|
||||
"type": "string",
|
||||
"description": "Page num",
|
||||
"required": false
|
||||
},
|
||||
{
|
||||
"name": "page[size]",
|
||||
"in": "query",
|
||||
"type": "string",
|
||||
"description": "Page size",
|
||||
"required": false
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Get search results",
|
||||
"schema": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "string",
|
||||
"description": "ID"
|
||||
},
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Type"
|
||||
},
|
||||
"attributes": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"record_count": {
|
||||
"type": "integer"
|
||||
},
|
||||
"page_count": {
|
||||
"type": "integer"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"tags": [
|
||||
"crops"
|
||||
],
|
||||
|
||||
18
yarn.lock
18
yarn.lock
@@ -735,9 +735,9 @@ fast-levenshtein@^2.0.6:
|
||||
integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
|
||||
|
||||
fast-uri@^3.0.1:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.0.tgz#66eecff6c764c0df9b762e62ca7edcfb53b4edfa"
|
||||
integrity sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.1.2.tgz#8af3d4fc9d3e71b11572cc2673b514a7d1a8c8ec"
|
||||
integrity sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==
|
||||
|
||||
fastq@^1.6.0:
|
||||
version "1.20.1"
|
||||
@@ -915,9 +915,9 @@ inherits@2, inherits@~2.0.3:
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
||||
ip-address@^10.0.1:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.1.0.tgz#d8dcffb34d0e02eb241427444a6e23f5b0595aa4"
|
||||
integrity sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-10.2.0.tgz#805fc178b20c518bd4c8548b24fe30892d7f3206"
|
||||
integrity sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==
|
||||
|
||||
is-arrayish@^0.2.1:
|
||||
version "0.2.1"
|
||||
@@ -1396,9 +1396,9 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
|
||||
|
||||
systeminformation@^5.25.11:
|
||||
version "5.31.5"
|
||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.5.tgz#e839fa6b40620a8bee010eb9d9d55c2d5f7042c8"
|
||||
integrity sha512-5SyLdip4/3alxD4Kh+63bUQTJmu7YMfYQTC+koZy7X73HgNqZSD2P4wOZQWtUncvPvcEmnfIjCoygN4MRoEejQ==
|
||||
version "5.31.6"
|
||||
resolved "https://registry.yarnpkg.com/systeminformation/-/systeminformation-5.31.6.tgz#2da4979a7262974fd068a3a306ded30aed6127c0"
|
||||
integrity sha512-Uv2b2uGGM6ns+26czgW2cYRabYdnswM0ddSOOlryHOaelzsmDSet1iM/NT7VOYxW8x/BW+HkY+b1Ve2pLTSGSA==
|
||||
|
||||
to-regex-range@^5.0.1:
|
||||
version "5.0.1"
|
||||
|
||||
Reference in New Issue
Block a user