Compare commits

..

43 Commits

Author SHA1 Message Date
Daniel O'Connor
186c2bc509 Merge branch 'dev' into rubocop 2025-09-20 19:07:37 +09:30
Daniel O'Connor
6df1d9d247 Merge pull request #4254 from Growstuff/feature/copy-activity
Add ability to copy/duplicate an activity
2025-09-20 19:06:43 +09:30
Daniel O'Connor
154d6e9022 Merge branch 'dev' into feature/copy-activity 2025-09-20 18:48:23 +09:30
Daniel O'Connor
dadaddccfb Merge pull request #4250 from Growstuff/dependabot/bundler/active_utils-3.6.0
Bump active_utils from 3.5.0 to 3.6.0
2025-09-20 18:44:51 +09:30
Daniel O'Connor
85f508b3a7 Merge pull request #4251 from Growstuff/dependabot/bundler/rexml-3.4.2
Bump rexml from 3.4.1 to 3.4.2
2025-09-20 18:44:39 +09:30
Daniel O'Connor
2e517519a1 Merge pull request #4252 from Growstuff/dependabot/bundler/recaptcha-5.21.1
Bump recaptcha from 5.20.1 to 5.21.1
2025-09-20 18:44:25 +09:30
google-labs-jules[bot]
5db6a86607 Add ability to copy/duplicate an activity 2025-09-20 08:54:12 +00:00
Daniel O'Connor
850687e80f Bump puma from 7.0.2 to 7.0.3 (#4249)
Bumps [puma](https://github.com/puma/puma) from 7.0.2 to 7.0.3.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v7.0.2...v7.0.3)

---
updated-dependencies:
- dependency-name: puma
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
2025-09-20 18:17:31 +09:30
Daniel O'Connor
1531c6e69b Merge branch 'dev' into dependabot/bundler/puma-7.0.3 2025-09-20 18:17:23 +09:30
Daniel O'Connor
ce7ce70d5f Merge pull request #4222 from Growstuff/feature/add-activity-prompts
feat: Add prompts for new activities
2025-09-20 17:49:47 +09:30
dependabot[bot]
600e61a282 Bump recaptcha from 5.20.1 to 5.21.1
Bumps [recaptcha](https://github.com/ambethia/recaptcha) from 5.20.1 to 5.21.1.
- [Changelog](https://github.com/ambethia/recaptcha/blob/master/CHANGELOG.md)
- [Commits](https://github.com/ambethia/recaptcha/compare/v5.20.1...v5.21.1)

---
updated-dependencies:
- dependency-name: recaptcha
  dependency-version: 5.21.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-18 07:01:16 +00:00
dependabot[bot]
aa7641ad91 Bump rexml from 3.4.1 to 3.4.2
Bumps [rexml](https://github.com/ruby/rexml) from 3.4.1 to 3.4.2.
- [Release notes](https://github.com/ruby/rexml/releases)
- [Changelog](https://github.com/ruby/rexml/blob/master/NEWS.md)
- [Commits](https://github.com/ruby/rexml/compare/v3.4.1...v3.4.2)

---
updated-dependencies:
- dependency-name: rexml
  dependency-version: 3.4.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-17 19:11:30 +00:00
dependabot[bot]
64acd4c00c Bump active_utils from 3.5.0 to 3.6.0
Bumps [active_utils](https://github.com/shopify/active_utils) from 3.5.0 to 3.6.0.
- [Release notes](https://github.com/shopify/active_utils/releases)
- [Changelog](https://github.com/Shopify/active_utils/blob/main/CHANGELOG.md)
- [Commits](https://github.com/shopify/active_utils/compare/v3.5.0...v3.6.0)

---
updated-dependencies:
- dependency-name: active_utils
  dependency-version: 3.6.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-16 07:01:15 +00:00
dependabot[bot]
d95ffdbef6 Bump puma from 7.0.2 to 7.0.3
Bumps [puma](https://github.com/puma/puma) from 7.0.2 to 7.0.3.
- [Release notes](https://github.com/puma/puma/releases)
- [Changelog](https://github.com/puma/puma/blob/master/History.md)
- [Commits](https://github.com/puma/puma/compare/v7.0.2...v7.0.3)

---
updated-dependencies:
- dependency-name: puma
  dependency-version: 7.0.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-09-15 07:38:14 +00:00
Daniel O'Connor
6d076983dd Merge pull request #4245 from Growstuff/swagger-format
Format
2025-09-12 17:41:23 +09:30
Daniel O'Connor
4dfe325077 Merge branch 'dev' into swagger-format 2025-09-12 08:12:01 +09:30
Daniel O'Connor
a76ef6a117 Format 2025-09-10 13:56:29 +00:00
Daniel O'Connor
99478e3920 Rubocop (#4242) 2025-09-10 20:46:12 +09:30
Daniel O'Connor
a2f05097af Merge branch 'mainline' into dev 2025-09-10 20:02:31 +09:30
Daniel O'Connor
e5bf9d98e6 Rubocop (#4241) 2025-09-10 19:56:12 +09:30
Daniel O'Connor
7988080054 Update .rubocop.yml 2025-09-10 19:52:44 +09:30
Daniel O'Connor
25da5a496b Merge branch 'rubocop-controllers' of https://github.com/Growstuff/growstuff into rubocop 2025-09-10 10:21:52 +00:00
Daniel O'Connor
ac5383ee38 Merge branch 'rubocop-lib' of https://github.com/Growstuff/growstuff into rubocop 2025-09-10 10:21:45 +00:00
Daniel O'Connor
c02fcc5405 Merge branch 'dev' of https://github.com/Growstuff/growstuff into rubocop 2025-09-10 10:21:14 +00:00
google-labs-jules[bot]
02db5b8130 Add API token generation, authentication, and CRUD for a number of the API resources (#4237)
* feat: Add API token generation and authentication

This commit introduces API token generation and authentication for write operations.

- Adds a section to the user's profile edit page to generate and display an API token.
- Reuses the `authentications` table to store the API token, avoiding the need for a database migration.
- Implements token-based authentication for the API using the `Authorization: Token token=...` header.
- Enables write operations for all API resources and ensures they are protected by the new authentication mechanism.
- Adds feature and request specs to test the new functionality.

* feat: Add API token generation and authentication

This commit introduces API token generation and authentication for write operations.

- Adds a section to the user's profile edit page to generate and display an API token.
- Reuses the `authentications` table to store the API token, avoiding the need for a database migration.
- Implements token-based authentication for the API using the `Authorization: Token token=...` header.
- Enables write operations for all API resources and ensures they are protected by the new authentication mechanism.
- Adds feature and request specs to test the new functionality.

* Mark as editable

* Refactor

* WIP - Authentication

* Implement more test coverage

* Split 401 and 403

* Before Create hooks

* Update harvest specs, defaulting to the first plant part - this may not be right

* Update coverage

* Update coverage

* Rubocop

* Rubocop

* Rubocop

* Fix coverage

* For now, mark photos immutable again

* Fix specs

* Fix specs

* Rubocop

* Fix specs

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
Co-authored-by: Daniel O'Connor <daniel.oconnor@gmail.com>
2025-09-10 19:50:06 +09:30
Daniel O'Connor
cf8380029a Rubocop 2025-09-10 10:19:08 +00:00
Daniel O'Connor
3c8cfc216e Rubocop 2025-09-10 10:16:58 +00:00
Daniel O'Connor
eefda21d1a Merge pull request #4226 from Growstuff/dev
Release 70
2025-09-09 22:23:27 +09:30
Daniel O'Connor
514d271638 Update rubocop 2025-09-07 08:48:02 +00:00
Daniel O'Connor
8ab94bae67 Update rubocop_todo 2025-09-07 08:47:03 +00:00
Daniel O'Connor
c6e57ca3ed Rubocop autocorrections 2025-09-07 08:41:26 +00:00
Daniel O'Connor
c3cdf8cb77 Rubocop safe autocorrections 2025-09-07 08:40:52 +00:00
Daniel O'Connor
82bcb00fa1 Rubocop safe autocorrections 2025-09-07 08:40:05 +00:00
Daniel O'Connor
aec2bb67e9 Rubocop autocorrections 2025-09-07 08:39:23 +00:00
Daniel O'Connor
60765d5ebf Rubocop autocorrections 2025-09-07 08:38:00 +00:00
Daniel O'Connor
baca600f45 Fix warnings 2025-09-07 08:37:15 +00:00
Daniel O'Connor
cfc486ce86 Merge pull request #4219 from Growstuff/dev
release 69.1
2025-09-07 15:07:58 +09:30
Daniel O'Connor
a900c2eb2f Merge pull request #4185 from Growstuff/dev
Release 69
2025-09-07 14:55:40 +09:30
Daniel O'Connor
29543d1d37 Release 68 (#4170)
* Improve menu again

* Fix crop button annoyance

* feat: Add PWA installation instructions to homepage

This commit adds instructions for mobile users on how to install the Growstuff website as a Progressive Web App (PWA).

The changes include:
- A new section on the homepage with instructions for both iOS and Android devices. This section is only visible to logged-out users.
- New translations for the instructions in the `en.yml` locale file.
- Basic styling for the new section.
- Updated feature tests to verify the new section's visibility.

* Restyle slightly

* Styling

* Github lure

* Make links bold, not all of the stats text

* Adjust specs

* Fix width of ready to harvest

* Update spec/features/home/home_spec.rb

* Fix display

* Fix text display wonkyness

* Merge pull request #4173 from Growstuff/translate-confirm

Garden Delete - Extract strings and fix missing translation bug

* Seeds for trade - avoid showing expired seeds on homepage. (#4176)

* Improve date visibility

* Ensure when seeding seeds, it's false

* Typo

---------

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
2025-08-31 15:23:16 +09:30
Daniel O'Connor
dfb791bf55 Merge pull request #4167 from Growstuff/dev
Release67, take 3
2025-08-30 01:16:01 +09:30
Daniel O'Connor
484797421e Merge pull request #4165 from Growstuff/dev
Release 67, attempt 2
2025-08-29 23:32:29 +09:30
Daniel O'Connor
a366d68c22 Merge pull request #4160 from Growstuff/dev
Release67 - September 2025?
2025-08-29 20:03:05 +09:30
Daniel O'Connor
e7dba3f0e9 Merge pull request #4147 from Growstuff/dev
August 24 Release (Release 66)
2025-08-24 17:02:03 +09:30
61 changed files with 3399 additions and 2672 deletions

View File

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

View File

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

View File

@@ -84,7 +84,7 @@ GEM
activesupport (>= 7.1)
active_record_union (1.3.0)
activerecord (>= 4.0)
active_utils (3.5.0)
active_utils (3.6.0)
activesupport (>= 4.2)
i18n
activejob (7.2.2.2)
@@ -475,7 +475,7 @@ GEM
date
stringio
public_suffix (6.0.1)
puma (7.0.2)
puma (7.0.3)
nio4r (~> 2.0)
query_diet (0.7.2)
racc (1.8.1)
@@ -543,7 +543,7 @@ GEM
rdoc (6.14.2)
erb
psych (>= 4.0.0)
recaptcha (5.20.1)
recaptcha (5.21.1)
redis-client (0.23.2)
connection_pool
regexp_parser (2.11.2)
@@ -557,7 +557,7 @@ GEM
http-cookie (>= 1.0.2, < 2.0)
mime-types (>= 1.16, < 4.0)
netrc (~> 0.8)
rexml (3.4.1)
rexml (3.4.2)
rouge (4.1.2)
rspec (3.13.0)
rspec-core (~> 3.13.0)

View File

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

View File

@@ -2,8 +2,6 @@
module Api
module V1
# This controller is intentionally empty.
# The `jsonapi-resources` gem provides the necessary actions.
class ActivitiesController < BaseController
end
end

View File

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

View File

@@ -39,7 +39,10 @@ class GardensController < DataController
def create
@garden.owner_id = current_member.id
flash[:notice] = I18n.t('gardens.created') if @garden.save
if @garden.save
link = new_activity_path(name: 'Weed the garden bed', garden_id: @garden.id, due_date: 2.weeks.from_now.to_date)
flash[:notice] = t('gardens.created_prompt_html', link: link).html_safe
end
respond_with(@garden)
end

View File

@@ -83,7 +83,12 @@ class PlantingsController < DataController
end
def update
@planting.update(planting_params)
if @planting.update(planting_params)
if planting_params[:finished].present? && @planting.garden.plantings.current.empty?
link = new_activity_path(name: 'Cultivate soil', garden_id: @planting.garden_id)
flash[:notice] = t('plantings.finished_prompt_html', link: link).html_safe
end
end
respond_with @planting
end

View File

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

View File

@@ -88,6 +88,19 @@ module ButtonsHelper
edit_button(edit_activity_path(activity), classes:)
end
def activity_copy_button(activity, classes: 'btn')
link_to new_activity_path(
name: activity.name,
description: activity.description,
category: activity.category,
garden_id: activity.garden_id,
planting_id: activity.planting_id,
due_date: activity.due_date
), class: classes do
copy_icon + ' ' + t('buttons.copy')
end
end
def activity_finish_button(activity, classes: 'btn btn-default btn-secondary')
return unless can?(:edit, activity) || activity.finished

View File

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

View File

@@ -90,7 +90,7 @@ class Crop < ApplicationRecord
def popular_plant_parts
PlantPart.joins(:harvests)
.where("crop_id = ?", id)
.order("count_harvests_id DESC")
.order(count_harvests_id: :desc)
.group("plant_parts.id", "plant_parts.name")
.count("harvests.id")
end

View File

@@ -24,6 +24,20 @@ class Member < ApplicationRecord
has_many :notifications, foreign_key: 'recipient_id', inverse_of: :recipient
has_many :sent_notifications, foreign_key: 'sender_id', inverse_of: :sender, class_name: "Notification"
has_many :authentications, dependent: :destroy
has_one :api_token, -> { where(provider: 'api') }, class_name: 'Authentication', dependent: :destroy
def api_token?
api_token.present?
end
def regenerate_api_token
api_token.destroy if api_token?
create_api_token(
provider: 'api',
uid: id,
token: SecureRandom.hex(16)
)
end
has_many :photos, inverse_of: :owner
has_many :likes, dependent: :destroy

View File

@@ -3,7 +3,9 @@
module Api
module V1
class ActivityResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :garden

View File

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

View File

@@ -3,7 +3,9 @@
module Api
module V1
class GardenResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_many :plantings

View File

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

View File

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

View File

@@ -3,7 +3,9 @@
module Api
module V1
class PlantingResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :garden
has_one :crop

View File

@@ -3,7 +3,9 @@
module Api
module V1
class SeedResource < BaseResource
immutable
before_create do
@model.owner = context[:current_user]
end
has_one :owner, class_name: 'Member'
has_one :crop

View File

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

View File

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

View File

@@ -15,3 +15,16 @@
method: :delete, class: "remove btn btn-danger"
- else
= link_to 'Connect to Flickr', '/members/auth/flickr', class: 'btn'
%hr
.row
.col-md-12
%p
= image_tag "icons/post.svg", size: "32x32", alt: 'API logo'
- if current_member.api_token?
Your API token is
%code= current_member.api_token.token
= link_to "Regenerate", regenerate_api_token_path,
data: { confirm: "Are you sure? Your old token will stop working immediately." },
method: :post, class: "remove btn btn-danger"
- else
= link_to 'Generate API Token', regenerate_api_token_path, method: :post, class: 'btn btn-primary'

View File

@@ -1,8 +1,10 @@
# frozen_string_literal: true
class UnauthorisedError < JSONAPI::Error
end
JSONAPI.configure do |config|
# built in paginators are :none, :offset, :paged
config.default_paginator = :offset
config.default_page_size = 10
config.maximum_page_size = 100
config.exception_class_whitelist = [CanCan::AccessDenied, UnauthorisedError]
end

View File

@@ -54,6 +54,7 @@ en:
You updated your account successfully, but we need to verify your new email address. Please check your email and click on the confirm
link to finalize confirming your new email address.
destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
api_token_regenerated: 'Your API token has been regenerated.'
unlocks:
send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
unlocked: 'Your account has been unlocked successfully. Please sign in to continue.'

View File

@@ -72,6 +72,7 @@ en:
add: Add
add_photo: Add photo
add_seed_to_stash: Add %{crop_name} seeds to stash
copy: Copy
delete: Delete
edit: Edit
harvest: Harvest
@@ -125,6 +126,7 @@ en:
updated: Garden was successfully updated.
confirm_delete: All plantings associated with this garden will also be deleted. Are you sure?
confirm_deactivate: All plantings associated with this garden will be marked as finished. Are you sure?
created_prompt_html: "Garden was successfully created. Would you like to <a href=\"%{link}\">plan to weed this garden bed in two weeks</a>?"
harvests:
created: Harvest was successfully created.
harvest_something: Harvest something
@@ -301,6 +303,7 @@ en:
finish_helper: >
An activity is finished when you've completed it, or it's otherwise
no longer possible.
finished_prompt_html: "Activity finished. Would you like to <a href=\"%{link}\">repeat this activity in two weeks</a>?"
plantings:
badges:
days_until_finished: days until finished
@@ -325,6 +328,7 @@ en:
string: "%{crop} planting in %{garden} by %{owner}"
progress:
progress_0_not_planted_yet: 'Progress: 0% - not planted yet'
finished_prompt_html: "Planting was successfully updated. Would you like to <a href=\"%{link}\">plan a soil cultivation activity</a>?"
posts:
write_blog_post: Write blog post
index:

View File

@@ -16,6 +16,7 @@ Rails.application.routes.draw do
}
devise_scope :member do
get '/members/unsubscribe/:message' => 'members#unsubscribe', as: 'unsubscribe_member'
post '/members/regenerate_api_token' => 'registrations#regenerate_api_token', as: 'regenerate_api_token'
end
match '/members/:id/finish_signup' => 'members#finish_signup', via: %i(get patch), as: :finish_signup

View File

@@ -2,10 +2,10 @@
class SetDefaultLanguageForExistingAlternateNames < ActiveRecord::Migration[7.2]
def up
AlternateName.update_all(language: 'en')
AlternateName.update_all(language: 'en') # rubocop:disable Rails/SkipsModelValidations
end
def down
AlternateName.update_all(language: nil)
AlternateName.update_all(language: nil) # rubocop:disable Rails/SkipsModelValidations
end
end

View File

@@ -36,21 +36,21 @@ namespace :wikidata do
aliases = wikidata_data['entities'][wikidata_id]['aliases']
aliases.each do |lang, values|
values.each do |value|
unless AlternateName.exists?(name: value['value'], language: lang, crop: crop)
AlternateName.create!(
name: value['value'],
language: lang,
crop: crop,
creator: creator
)
puts " Added alternate name: #{value['value']} (#{lang})"
end
next if AlternateName.exists?(name: value['value'], language: lang, crop: crop)
AlternateName.create!(
name: value['value'],
language: lang,
crop: crop,
creator: creator
)
puts " Added alternate name: #{value['value']} (#{lang})"
end
end
else
puts " Could not find Wikidata ID for #{crop.name}"
end
rescue => e
rescue StandardError => e
puts " Error processing crop #{crop.name}: #{e.message}"
end
end

View File

@@ -4,6 +4,7 @@ require 'rails_helper'
RSpec.describe GardenTypesController, type: :controller do
include Devise::Test::ControllerHelpers
let(:valid_params) { { name: 'My second GardenType' } }
let(:garden_type) { FactoryBot.create(:garden_type) }

View File

@@ -4,6 +4,7 @@ require 'rails_helper'
RSpec.describe GardensController, type: :controller do
include Devise::Test::ControllerHelpers
let(:valid_params) { { name: 'My second Garden' } }
let(:garden) { FactoryBot.create(:garden) }

View File

@@ -2,7 +2,7 @@
FactoryBot.define do
factory :comment do
association :commentable, factory: :post
commentable factory: %i(post)
author
sequence(:body) { |n| "OMG LOL #{n}" }
# because our commenters are more polite than YouTube's

View File

@@ -1,3 +1,5 @@
# frozen_string_literal: true
FactoryBot.define do
factory :garden_collaborator do
garden

View File

@@ -10,7 +10,7 @@ FactoryBot.define do
body { "MyText" }
read { false }
association :notifiable, factory: :post
notifiable factory: %i(post)
factory :no_email_notification do
recipient { FactoryBot.create(:no_email_notifications_member) }

View File

@@ -18,7 +18,7 @@ describe "Conversations", :js do
click_link 'Inbox'
end
include_examples 'is accessible'
it_behaves_like 'is accessible'
it { expect(page).to have_content 'something i want to say' }
it { page.percy_snapshot(page, name: 'conversations#index') }

View File

@@ -83,23 +83,23 @@ describe "Alternate names", :js do
end
context 'Anonymous' do
include_examples 'show alt names'
it_behaves_like 'show alt names'
end
context 'Signed in member' do
include_context 'signed in member'
include_examples 'show alt names'
it_behaves_like 'show alt names'
end
context 'Crop wrangler' do
include_context 'signed in crop wrangler'
include_examples 'show alt names'
include_examples 'edit alt names'
it_behaves_like 'show alt names'
it_behaves_like 'edit alt names'
end
context 'Admin' do
include_context 'signed in admin'
include_examples 'show alt names'
include_examples 'edit alt names'
it_behaves_like 'show alt names'
it_behaves_like 'edit alt names'
end
end

View File

@@ -34,25 +34,25 @@ describe "browse crops", :search do
end
context 'anon' do
include_examples 'shows crops'
it_behaves_like 'shows crops'
it { expect(page).to have_no_link "Add New Crop" }
end
context 'member' do
include_context 'signed in member'
include_examples 'shows crops'
include_examples 'add new crop'
it_behaves_like 'shows crops'
it_behaves_like 'add new crop'
end
context 'wrangler' do
include_context 'signed in crop wrangler'
include_examples 'shows crops'
include_examples 'add new crop'
it_behaves_like 'shows crops'
it_behaves_like 'add new crop'
end
context 'admin' do
include_context 'signed in admin'
include_examples 'shows crops'
include_examples 'add new crop'
it_behaves_like 'shows crops'
it_behaves_like 'add new crop'
end
end

View File

@@ -33,7 +33,7 @@ describe "Crop", :js do
shared_examples 'request crop' do
describe "requesting a crop with multiple scientific and alternate name" do
include_examples 'fill in form'
it_behaves_like 'fill in form'
before do
within "form#new_crop" do
fill_in "request_notes", with: "This is the Philippine national flower."
@@ -50,7 +50,7 @@ describe "Crop", :js do
shared_examples 'create crop' do
describe "creating a crop with multiple scientific and alternate name" do
include_examples 'fill in form'
it_behaves_like 'fill in form'
before do
click_button "Save"
end
@@ -69,16 +69,16 @@ describe "Crop", :js do
context 'member' do
include_context 'signed in member'
include_examples 'request crop'
it_behaves_like 'request crop'
end
context 'crop wrangler' do
include_context 'signed in crop wrangler'
include_examples 'create crop'
it_behaves_like 'create crop'
end
context 'admin' do
include_context 'signed in admin'
include_examples 'create crop'
it_behaves_like 'create crop'
end
end

View File

@@ -59,17 +59,17 @@ describe "crop detail page", :js, :search do
context "when signed in" do
include_context 'signed in member'
include_examples "shows photos"
it_behaves_like "shows photos"
end
context "when signed in as photos owner" do
include_context 'signed in member'
let(:member) { owner_member }
include_examples "shows photos"
it_behaves_like "shows photos"
end
context "when not signed in" do
include_examples "shows photos"
it_behaves_like "shows photos"
end
end

View File

@@ -27,11 +27,11 @@ describe "Delete crop spec" do
context "As a crop wrangler" do
include_context 'signed in crop wrangler'
include_examples 'delete crop'
it_behaves_like 'delete crop'
end
context 'admin' do
include_context 'signed in admin'
include_examples 'delete crop'
it_behaves_like 'delete crop'
end
end

View File

@@ -25,7 +25,7 @@ describe "Gardens" do
context 'my gardens' do
before { visit gardens_path(member_slug: member.slug) }
include_examples "has buttons bar at top"
it_behaves_like "has buttons bar at top"
context 'with actions menu expanded' do
before { click_link 'Actions' }
@@ -43,13 +43,13 @@ describe "Gardens" do
context 'all gardens' do
before { visit gardens_path }
include_examples "has buttons bar at top"
it_behaves_like "has buttons bar at top"
end
context "other member's garden" do
before { visit gardens_path(member_slug: FactoryBot.create(:member).slug) }
include_examples "has buttons bar at top"
it_behaves_like "has buttons bar at top"
describe 'does not show actions on other member garden' do
it { is_expected.to have_no_link 'Actions' }
end

View File

@@ -8,7 +8,7 @@ describe "Gardens", :js do
include_context 'signed in member'
before { visit new_garden_path }
include_examples 'is accessible'
it_behaves_like 'is accessible'
it "displays required and optional fields properly" do
expect(page).to have_selector ".required", text: "Name"

View File

@@ -14,7 +14,7 @@ describe "Gardens#index", :js do
visit member_gardens_path(member_slug: member.slug)
end
include_examples 'is accessible'
it_behaves_like 'is accessible'
it "displays each of the gardens" do
member.gardens.each do |garden|

View File

@@ -26,7 +26,7 @@ describe "Harvesting a crop", :js, :search do
within "form#new_harvest" do
choose plant_part.name
fill_in "When?", with: Time.new(2014, 06, 15)
fill_in "When?", with: Time.zone.local(2014, 0o6, 15)
fill_in "How many?", with: 42
fill_in "Weighing (in total)", with: 42
fill_in "Notes", with: "It's killer."

View File

@@ -58,13 +58,13 @@ describe 'Likeable', :js, :search do
describe 'photos#index' do
let(:path) { photos_path }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
describe 'photos#show' do
let(:path) { photo_path(photo) }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
describe 'crops#show' do
@@ -74,7 +74,7 @@ describe 'Likeable', :js, :search do
before { planting.photos << photo }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
end
@@ -82,27 +82,27 @@ describe 'Likeable', :js, :search do
let(:like_count_class) { ".post-#{post.id} .like-count" }
let(:path) { post_path(post) }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
describe 'activities' do
let(:like_count_class) { ".activity-#{activity.id} .like-count" }
let(:path) { activity_path(activity) }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
describe 'plantings' do
let(:like_count_class) { ".planting-#{planting.id} .like-count" }
let(:path) { planting_path(planting) }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
describe 'harvests' do
let(:like_count_class) { ".harvest-#{harvest.id} .like-count" }
let(:path) { harvest_path(harvest) }
include_examples 'object can be liked'
it_behaves_like 'object can be liked'
end
end

View File

@@ -2,7 +2,7 @@
require 'rails_helper'
describe "member deletion", flaky: true do
describe "member deletion", :flaky do
context "with activity and followers" do
let(:member) { FactoryBot.create(:member) }
let(:other_member) { FactoryBot.create(:member) }
@@ -63,7 +63,7 @@ describe "member deletion", flaky: true do
member.reload
expect(member.discarded?).to be true
# Frustratingly, this cannot be discarded? and also meet
# Frustratingly, this cannot be discarded? and also meet
# `@member = Member.confirmed.kept.find_by!(slug: params[:slug])`
#
# Yet, we see the below assert fail in CI.
@@ -96,7 +96,7 @@ describe "member deletion", flaky: true do
end
describe 'member exists but is marked deleted' do
subject { Member.all.find(member.id) }
subject { Member.find(member.id) }
it { expect(subject).to eq member }
it { expect(subject.discarded?).to be true }

View File

@@ -0,0 +1,38 @@
# frozen_string_literal: true
require 'rails_helper'
describe "member token management", :js do
include_context 'signed in member'
before do
visit edit_member_registration_path
click_on "Apps"
end
it "can generate an API token" do
expect(page).to have_no_content("Your API token is")
click_on "Generate API Token"
expect(page).to have_content("Your API token is")
member.reload
expect(member.api_token).to be_present
end
context "with an existing token" do
before do
member.regenerate_api_token
visit edit_member_registration_path
click_on "Apps"
end
it "can regenerate an API token" do
old_token = member.api_token.token
expect(page).to have_content("Your API token is")
accept_confirm do
click_on "Regenerate"
end
expect(page).to have_content("Your API token is")
expect(member.reload.api_token.token).not_to eq(old_token)
end
end
end

View File

@@ -187,7 +187,7 @@ describe "Planting a crop", :js, :search do
check "finished"
fill_in "Finished date", with: "2015-06-25"
click_button "Save"
expect(page).to have_content "planting was successfully updated"
expect(page).to have_content "Planting was successfully updated"
expect(page).to have_content "Finished"
end

View File

@@ -34,11 +34,11 @@ describe "signout" do
end
describe 'after signout, redirect to signin page if page needs authentication' do
include_examples "sign-in redirects", "/plantings/new"
include_examples "sign-in redirects", "/harvests/new"
include_examples "sign-in redirects", "/posts/new"
include_examples "sign-in redirects", "/gardens/new"
include_examples "sign-in redirects", "/seeds/new"
it_behaves_like "sign-in redirects", "/plantings/new"
it_behaves_like "sign-in redirects", "/harvests/new"
it_behaves_like "sign-in redirects", "/posts/new"
it_behaves_like "sign-in redirects", "/gardens/new"
it_behaves_like "sign-in redirects", "/seeds/new"
end
it 'photos' do

View File

@@ -154,7 +154,7 @@ describe Crop do
it { expect(crop.default_photo).to eq photo }
include_examples 'has default photo'
it_behaves_like 'has default photo'
end
context 'with a harvest photo' do
@@ -165,7 +165,7 @@ describe Crop do
it { expect(crop.default_photo).to eq photo }
include_examples 'has default photo'
it_behaves_like 'has default photo'
context 'and planting photo' do
let(:planting) { FactoryBot.create(:planting, crop:) }

View File

@@ -523,6 +523,7 @@ describe Planting do
context "failed" do
let(:failed_planting) { FactoryBot.create(:planting, failed: true) }
it 'has a failed field' do
expect(failed_planting.failed).to be true
end
@@ -535,20 +536,20 @@ describe Planting do
end
it 'is not included in the active scope' do
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:planting, failed: true)
described_class.active.should include @p
described_class.active.should_not include @f
@p = FactoryBot.create(:planting)
@f = FactoryBot.create(:planting, failed: true)
described_class.active.should include @p
described_class.active.should_not include @f
end
it 'cannot be finished and failed' do
@f = FactoryBot.build(:planting, finished: true, failed: true)
@f.should_not be_valid
@f = FactoryBot.build(:planting, finished: true, failed: true)
@f.should_not be_valid
end
it 'is not finished' do
@f = FactoryBot.build(:planting, finished: true, failed: true)
expect(@f.finished?).to be false
@f = FactoryBot.build(:planting, finished: true, failed: true)
expect(@f.finished?).to be false
end
end
@@ -587,7 +588,7 @@ describe Planting do
FactoryBot.create(:finished_planting, owner: member, garden: member.gardens.first)
end
let!(:failed_planting) do
FactoryBot.create(:planting, failed: true, owner: member, garden: member.gardens.first)
FactoryBot.create(:planting, failed: true, owner: member, garden: member.gardens.first)
end
it { expect(member.plantings.active).to include(planting) }

View File

@@ -70,8 +70,8 @@ include Warden::Test::Helpers
# directory. Alternatively, in the individual `*_spec.rb` files, manually
# require only the support files necessary.
#
Dir[Rails.root.join("spec/support/**/*.rb")].sort.each { |f| require f }
Dir[Rails.root.join("spec/features/shared_examples/**/*.rb")].sort.each { |f| require f }
Rails.root.glob("spec/support/**/*.rb").sort.each { |f| require f }
Rails.root.glob("spec/features/shared_examples/**/*.rb").sort.each { |f| require f }
# Checks for pending migrations before tests are run.
# If you are not using ActiveRecord, you can remove this line.

View File

@@ -23,34 +23,34 @@ RSpec.describe 'Activities', type: :request do
it 'filters by owner' do
get("/api/v1/activities?filter[owner-id]=#{activity.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by garden' do
get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:)
get("/api/v1/activities?filter[garden-id]=#{activity.garden.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by planting' do
get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:)
get("/api/v1/activities?filter[planting-id]=#{activity.planting.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
end
it 'filters by category' do
get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:)
get("/api/v1/activities?filter[category]=#{activity.category}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(subject['data'][1]['id']).to eq(activity2.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(activity.id.to_s)
expect(subject['data'][1]['id']).to eq(activity2.id.to_s)
end
end
end

View File

@@ -52,18 +52,19 @@ RSpec.describe 'Gardens', type: :request do
context 'filtering' do
let!(:garden2) { FactoryBot.create(:garden, active: false, garden_type: FactoryBot.create(:garden_type)) }
pending 'filters by active' do
get('/api/v1/gardens?filter[active]=true', params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(garden.id.to_s)
end
it 'filters by garden_type' do
get("/api/v1/gardens?filter[garden_type]=#{garden2.garden_type.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(garden2.id.to_s)
end
@@ -71,27 +72,116 @@ RSpec.describe 'Gardens', type: :request do
it 'filters by owner' do
get("/api/v1/gardens?filter[owner_id]=#{garden2.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'][1]['id']).to eq(garden2.id.to_s)
end
end
it '#create' do
expect do
post '/api/v1/gardens', params: { 'garden' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:garden_params) do
{
data: {
type: 'gardens',
attributes: {
name: 'My API Garden'
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/gardens', params: garden_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/gardens', params: garden_params, headers: auth_headers
expect(response).to have_http_status(:created)
expect(member.gardens.count).to eq(2) # 1 from after_create callback, 1 from api
end
end
it '#update' do
expect do
post "/api/v1/gardens/#{garden.id}", params: { 'garden' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:garden) { create(:garden, owner: member) }
let(:other_member_garden) { create(:garden) }
let(:update_params) do
{
data: {
type: 'gardens',
id: garden.id.to_s,
attributes: {
name: 'An updated garden'
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own garden' do
patch "/api/v1/gardens/#{garden.id}", params: update_params, headers: auth_headers
expect(response).to have_http_status(:ok)
expect(garden.reload.name).to eq('An updated garden')
end
it 'returns 403 Forbidden for another member\'s garden' do
update_params_for_other = {
data: {
type: 'gardens',
id: other_member_garden.id.to_s,
attributes: {
name: 'An updated garden'
}
}
}.to_json
patch "/api/v1/gardens/#{other_member_garden.id}", params: update_params_for_other, headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
it '#delete' do
expect do
delete "/api/v1/gardens/#{garden.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:garden) { create(:garden, owner: member) }
let(:other_member_garden) { create(:garden) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/gardens/#{garden.id}", headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own garden' do
delete "/api/v1/gardens/#{garden.id}", headers: auth_headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: garden.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s garden' do
delete "/api/v1/gardens/#{other_member_garden.id}", headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
end

View File

@@ -78,6 +78,7 @@ RSpec.describe 'Harvests', type: :request do
context 'filtering' do
let!(:harvest2) { FactoryBot.create(:harvest, planting: create(:planting)) }
it 'filters by crop' do
get("/api/v1/harvests?filter[crop_id]=#{harvest2.crop.id}", params: {}, headers:)
expect(subject['data'].size).to eq(1)
@@ -87,47 +88,141 @@ RSpec.describe 'Harvests', type: :request do
it 'filters by planting' do
get("/api/v1/harvests?filter[planting_id]=#{harvest2.planting.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
it 'filters by plant_part' do
get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:)
get("/api/v1/harvests?filter[plant_part]=#{harvest2.plant_part.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
it 'filters by owner' do
get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:)
get("/api/v1/harvests?filter[owner_id]=#{harvest2.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(harvest2.id.to_s)
end
end
it '#create' do
expect do
put '/api/v1/harvests', headers:, params: {
'harvest' => { 'description' => 'can i make this' }
}
end.to raise_error ActionController::RoutingError
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:planting) { create(:planting, owner: member) }
let(:plant_part) { create(:plant_part) }
let(:harvest_params) do
{
data: {
type: 'harvests',
attributes: {
description: 'My API harvests'
},
relationships: {
planting: { data: { type: 'plantings', id: planting.id } }
# plant_part: { data: { type: 'plant_parts', id: plant_part.id } }
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/harvests', params: harvest_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/harvests', params: harvest_params, headers: auth_headers
expect(response).to have_http_status(:created)
expect(member.harvests.count).to eq(1)
end
end
it '#update' do
expect do
post "/api/v1/harvests/#{harvest.id}", headers:, params: {
'harvest' => { 'description' => 'can i modify this' }
}
end.to raise_error ActionController::RoutingError
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:harvest) { create(:harvest, owner: member) }
let(:other_member_harvest) { create(:harvest) }
let(:update_params) do
{
data: {
type: 'harvests',
id: harvest.id.to_s,
attributes: {
description: 'An updated harvest'
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own harvest' do
patch "/api/v1/harvests/#{harvest.id}", params: update_params, headers: auth_headers
expect(response).to have_http_status(:ok)
expect(harvest.reload.description).to eq('An updated harvest')
end
it 'returns 403 Forbidden for another member\'s harvest' do
update_params_for_other = {
data: {
type: 'harvests',
id: other_member_harvest.id.to_s,
attributes: {
description: 'An updated harvest'
}
}
}.to_json
patch "/api/v1/harvests/#{other_member_harvest.id}", params: update_params_for_other, headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
it '#delete' do
expect do
delete "/api/v1/harvests/#{harvest.id}", headers:, params: {}
end.to raise_error ActionController::RoutingError
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:harvest) { create(:harvest, owner: member) }
let(:other_member_harvest) { create(:harvest) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/harvests/#{harvest.id}", headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own harvest' do
delete "/api/v1/harvests/#{harvest.id}", headers: auth_headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: harvest.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s harvest' do
delete "/api/v1/harvests/#{other_member_harvest.id}", headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
end

View File

@@ -95,24 +95,119 @@ RSpec.describe 'Plantings', type: :request do
expect(subject['data']).to eq(planting_encoded_as_json_api)
end
it '#create' do
expect do
post '/api/v1/plantings', params: { 'planting' => { 'description' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:garden) { create(:garden, owner: member) }
let(:planting_params) do
{
data: {
type: 'plantings',
attributes: {
description: 'My API plantings'
},
relationships: {
crop: { data: { type: 'crops', id: crop.id } },
garden: { data: { type: 'gardens', id: garden.id } }
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/plantings', params: planting_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/plantings', params: planting_params, headers: auth_headers
expect(response).to have_http_status(:created)
expect(member.plantings.count).to eq(1)
end
end
it '#update' do
expect do
post "/api/v1/plantings/#{planting.id}", headers:, params: {
'planting' => { 'description' => 'can i modify this' }
}
end.to raise_error ActionController::RoutingError
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:planting) { create(:planting, owner: member) }
let(:other_member_planting) { create(:planting) }
let(:update_params) do
{
data: {
type: 'plantings',
id: planting.id.to_s,
attributes: {
description: 'An updated planting'
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own planting' do
patch "/api/v1/plantings/#{planting.id}", params: update_params, headers: auth_headers
expect(response).to have_http_status(:ok)
expect(planting.reload.description).to eq('An updated planting')
end
it 'returns 403 Forbidden for another member\'s planting' do
update_params_for_other = {
data: {
type: 'plantings',
id: other_member_planting.id.to_s,
attributes: {
description: 'An updated planting'
}
}
}.to_json
patch "/api/v1/plantings/#{other_member_planting.id}", params: update_params_for_other, headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
it '#delete' do
expect do
delete "/api/v1/plantings/#{planting.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let!(:planting) { create(:planting, owner: member) }
let(:other_member_planting) { create(:planting) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/plantings/#{planting.id}", headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own planting' do
delete "/api/v1/plantings/#{planting.id}", headers: auth_headers
expect(response).to have_http_status(:no_content)
expect(Garden.find_by(id: planting.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s planting' do
delete "/api/v1/plantings/#{other_member_planting.id}", headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
describe "by member/owner" do
@@ -144,6 +239,7 @@ RSpec.describe 'Plantings', type: :request do
context 'filtering' do
let!(:planting2) { FactoryBot.create(:planting, failed: true, sunniness: 'shade') }
let!(:perennial_planting) { FactoryBot.create(:planting, crop: FactoryBot.create(:crop, perennial: true)) }
it 'filters by failed' do
get('/api/v1/plantings?filter[failed]=true', params: {}, headers:)
expect(subject['data'].size).to eq(1)
@@ -151,25 +247,25 @@ RSpec.describe 'Plantings', type: :request do
end
it 'filters by sunniness' do
get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
get('/api/v1/plantings?filter[sunniness]=shade', params: {}, headers:)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(planting2.id.to_s)
end
it 'filters by perennial' do
get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:)
get('/api/v1/plantings?filter[perennial]=true', params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(perennial_planting.id.to_s)
end
it 'filters by active' do
get('/api/v1/plantings?filter[active]=true', params: {}, headers:)
get('/api/v1/plantings?filter[active]=true', params: {}, headers:)
expect(response.status).to eq 200
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(2)
expect(subject['data'][0]['id']).to eq(planting.id.to_s)
end
end
end

View File

@@ -61,39 +61,136 @@ RSpec.describe 'Seeds', type: :request do
it { expect(subject['data']).to eq(seed_encoded_as_json_api) }
end
it '#create' do
expect do
post '/api/v1/seeds', params: { 'seed' => { 'name' => 'can i make this' } }, headers:
end.to raise_error ActionController::RoutingError
describe '#create' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:seed_params) do
{
data: {
type: 'seeds',
attributes: {
description: 'My API seeds'
},
relationships: {
crop: { data: { type: 'crops', id: crop.id } }
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
post '/api/v1/seeds', params: seed_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 201 Created with a valid token' do
post '/api/v1/seeds', params: seed_params, headers: auth_headers
expect(response).to have_http_status(:created)
expect(member.seeds.count).to eq(1)
end
end
it '#update' do
expect do
post "/api/v1/seeds/#{seed.id}", params: { 'seed' => { 'name' => 'can i modify this' } }, headers:
end.to raise_error ActionController::RoutingError
describe '#update' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let(:seed) { create(:seed, owner: member, crop: crop) }
let(:other_member_seed) { create(:seed) }
let(:update_params) do
{
data: {
type: 'seeds',
id: seed.id.to_s,
attributes: {
description: 'An updated seed'
}
}
}.to_json
end
it 'returns 401 Unauthorized without a token' do
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 200 OK with a valid token for own seed' do
patch "/api/v1/seeds/#{seed.id}", params: update_params, headers: auth_headers
expect(response).to have_http_status(:ok)
expect(seed.reload.description).to eq('An updated seed')
end
it 'returns 403 Forbidden for another member\'s seed' do
update_params_for_other = {
data: {
type: 'seeds',
id: other_member_seed.id.to_s,
attributes: {
description: 'An updated seed'
}
}
}.to_json
patch "/api/v1/seeds/#{other_member_seed.id}", params: update_params_for_other, headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
it '#delete' do
expect do
delete "/api/v1/seeds/#{seed.id}", params: {}, headers:
end.to raise_error ActionController::RoutingError
describe '#delete' do
let!(:member) { create(:member) }
let(:token) do
member.regenerate_api_token
member.api_token.token
end
let(:headers) { { 'Accept' => 'application/vnd.api+json', 'Content-Type' => 'application/vnd.api+json' } }
let(:auth_headers) { headers.merge('Authorization' => "Token token=#{token}") }
let(:crop) { create(:crop) }
let!(:seed) { create(:seed, owner: member, crop: crop) }
let(:other_member_seed) { create(:seed) }
it 'returns 401 Unauthorized without a token' do
delete "/api/v1/seeds/#{seed.id}", headers: headers
expect(response).to have_http_status(:unauthorized)
end
it 'returns 204 No Content with a valid token for own seed' do
delete "/api/v1/seeds/#{seed.id}", headers: auth_headers
expect(response).to have_http_status(:no_content)
expect(Seed.find_by(id: seed.id)).to be_nil
end
it 'returns 403 Forbidden for another member\'s seed' do
delete "/api/v1/seeds/#{other_member_seed.id}", headers: auth_headers
expect(response).to have_http_status(:forbidden)
end
end
context 'filtering' do
let!(:seed2) { FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom') }
let!(:seed2) do
FactoryBot.create(:seed, tradable_to: 'nationally', organic: 'certified organic', gmo: 'certified GMO-free', heirloom: 'heirloom')
end
it 'filters by crop' do
get("/api/v1/seeds?filter[crop]=#{seed2.crop.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
it 'filters by tradable_to' do
get('/api/v1/seeds?filter[tradable_to]=nationally', params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
@@ -101,7 +198,7 @@ RSpec.describe 'Seeds', type: :request do
it 'filters by organic' do
get('/api/v1/seeds?filter[organic]=certified organic', params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
@@ -109,7 +206,7 @@ RSpec.describe 'Seeds', type: :request do
it 'filters by gmo' do
get('/api/v1/seeds?filter[gmo]=certified GMO-free', params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
@@ -117,7 +214,7 @@ RSpec.describe 'Seeds', type: :request do
it 'filters by heirloom' do
get('/api/v1/seeds?filter[heirloom]=heirloom', params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end
@@ -125,7 +222,7 @@ RSpec.describe 'Seeds', type: :request do
it 'filters by owner' do
get("/api/v1/seeds?filter[owner_id]=#{seed2.owner.id}", params: {}, headers:)
expect(response.status).to eq 200
expect(response).to have_http_status(:ok)
expect(subject['data'].size).to eq(1)
expect(subject['data'][0]['id']).to eq(seed2.id.to_s)
end

View File

@@ -14,17 +14,17 @@ module FeatureHelpers
shared_context 'signed in member' do
let(:member) { FactoryBot.create(:member) }
include_examples 'sign in'
it_behaves_like 'sign in'
end
shared_context 'signed in crop wrangler' do
let(:member) { FactoryBot.create(:crop_wrangling_member) }
include_examples 'sign in'
it_behaves_like 'sign in'
end
shared_context 'signed in admin' do
let(:member) { FactoryBot.create(:admin_member) }
include_examples 'sign in'
it_behaves_like 'sign in'
end
shared_context 'sign in' do

View File

@@ -58,7 +58,7 @@ describe "photos/show" do
render
end
include_examples "photo data renders"
it_behaves_like "photo data renders"
it "has a delete button" do
assert_select "a[href='#{photo_path(@photo)}']"
@@ -71,8 +71,8 @@ describe "photos/show" do
render
end
include_examples "photo data renders"
include_examples "No links to change data"
it_behaves_like "photo data renders"
it_behaves_like "No links to change data"
end
context "not signed in" do
@@ -81,8 +81,8 @@ describe "photos/show" do
render
end
include_examples "photo data renders"
include_examples "No links to change data"
it_behaves_like "photo data renders"
it_behaves_like "No links to change data"
end
context "CC-licensed photo" do

View File

@@ -31,7 +31,7 @@ describe 'seeds/index.rss.haml', :search do
render
end
include_examples 'displays seed in rss feed'
it_behaves_like 'displays seed in rss feed'
it 'shows RSS feed title' do
expect(rendered).to have_content "Recent seeds from all members"
@@ -60,6 +60,6 @@ describe 'seeds/index.rss.haml', :search do
expect(rendered).to have_content "Recent seeds from #{seed.owner}"
end
include_examples 'displays seed in rss feed'
it_behaves_like 'displays seed in rss feed'
end
end

View File

File diff suppressed because it is too large Load Diff