Compare commits

..

2 Commits

Author SHA1 Message Date
Daniel O'Connor
10e5fb8fb3 Merge branch 'dev' into fix/preserve-scientific-name-attributes 2026-04-25 10:10:04 +09:30
google-labs-jules[bot]
82a9b58ab3 Fix: Preserve scientific name attributes on crop edit
When editing a crop, the `recreate_names` method in the `CropsController`
would always destroy and recreate the associated scientific names, even if
they had not changed. This caused any additional attributes on the
scientific names to be lost.

This commit modifies `recreate_names` to first check if the submitted
names are different from the existing ones. If they are the same, the
method returns early, preserving the existing records and their
attributes.

Additionally, this commit corrects a typo in `crop_params` where
`:scientific_name` was used instead of `:name` for the nested attributes,
ensuring that the parameters are permitted correctly.
2025-12-02 13:09:36 +00:00
80 changed files with 460 additions and 1281 deletions

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
# 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-03-01 05:17:50 UTC using RuboCop version 1.85.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -81,7 +81,7 @@ Layout/HashAlignment:
- 'spec/requests/api/v1/activities_request_spec.rb'
- 'spec/requests/api/v1/members_request_spec.rb'
# Offense count: 5
# Offense count: 6
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: Max, AllowHeredoc, AllowURI, AllowQualifiedName, URISchemes, AllowRBSInlineAnnotation, AllowCopDirectives, AllowedPatterns, SplitStrings.
# URISchemes: http, https
@@ -92,6 +92,7 @@ Layout/LineLength:
- 'app/models/concerns/predict_planting.rb'
- 'app/models/crop.rb'
- 'db/seeds.rb'
- 'spec/requests/api/v1/activities_request_spec.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
@@ -153,6 +154,21 @@ Lint/SuppressedException:
Exclude:
- 'lib/tasks/testing.rake'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: strict, consistent
Lint/SymbolConversion:
Exclude:
- 'app/helpers/crops_helper.rb'
# Offense count: 7
# This cop supports safe autocorrection (--autocorrect).
Lint/UselessAssignment:
Exclude:
- 'config.rb'
- 'config/compass.rb'
# Offense count: 1
Lint/UselessConstantScoping:
Exclude:
@@ -226,12 +242,18 @@ RSpec/BeforeAfterAll:
Exclude:
- 'spec/tasks/import_spec.rb'
# Offense count: 298
# Offense count: 299
# Configuration parameters: Prefixes, AllowedPatterns.
# Prefixes: when, with, without
RSpec/ContextWording:
Enabled: false
# Offense count: 1
# Configuration parameters: IgnoredMetadata.
RSpec/DescribeClass:
Exclude:
- 'spec/tasks/import_spec.rb'
# Offense count: 36
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: SkipBlocks, EnforcedStyle, OnlyStaticConstants.
@@ -242,12 +264,47 @@ RSpec/DescribedClass:
- 'spec/models/member_spec.rb'
- 'spec/services/timeline_service_spec.rb'
# Offense count: 13
# This cop supports unsafe autocorrection (--autocorrect-all).
RSpec/EmptyExampleGroup:
Exclude:
- 'spec/controllers/authentications_controller_spec.rb'
- 'spec/controllers/forums_controller_spec.rb'
- 'spec/controllers/home_controller_spec.rb'
- 'spec/controllers/likes_controller_spec.rb'
- 'spec/controllers/plant_parts_controller_spec.rb'
- 'spec/controllers/seeds_controller_spec.rb'
- 'spec/features/crops/crop_detail_page_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/requests/authentications_spec.rb'
- 'spec/views/home/index_spec.rb'
- 'spec/views/photos/edit.html.haml_spec.rb'
- 'spec/views/posts/_single.html.haml_spec.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowConsecutiveOneLiners.
RSpec/EmptyLineAfterExample:
Exclude:
- 'spec/models/ability_spec.rb'
# Offense count: 146
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 27
# Offense count: 32
RSpec/ExpectInHook:
Exclude:
- 'spec/controllers/garden_types_controller_spec.rb'
- 'spec/controllers/gardens_controller_spec.rb'
- 'spec/features/admin/forums_spec.rb'
- 'spec/features/admin/plant_parts_spec.rb'
- 'spec/features/admin/roles_spec.rb'
- 'spec/features/crops/crop_photos_spec.rb'
- 'spec/features/members/list_spec.rb'
- 'spec/features/plantings/planting_a_crop_spec.rb'
- 'spec/features/shared_examples/append_date.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
@@ -295,6 +352,7 @@ RSpec/IndexedLet:
- 'spec/features/percy/percy_spec.rb'
- 'spec/features/planting_reminder_spec.rb'
- 'spec/features/timeline/index_spec.rb'
- 'spec/models/crop_spec.rb'
- 'spec/models/member_spec.rb'
- 'spec/views/forums/index.html.haml_spec.rb'
@@ -329,7 +387,7 @@ RSpec/MultipleDescribes:
Exclude:
- 'spec/features/crops/crop_wranglers_spec.rb'
# Offense count: 191
# Offense count: 189
RSpec/MultipleExpectations:
Max: 19
@@ -344,7 +402,7 @@ RSpec/MultipleMemoizedHelpers:
RSpec/NamedSubject:
Enabled: false
# Offense count: 109
# Offense count: 111
# Configuration parameters: AllowedGroups.
RSpec/NestedGroups:
Max: 6
@@ -475,6 +533,10 @@ Rails/I18nLocaleAssignment:
Exclude:
- 'spec/features/locale_spec.rb'
# Offense count: 40
Rails/I18nLocaleTexts:
Enabled: false
# Offense count: 1
# Configuration parameters: IgnoreScopes.
Rails/InverseOf:
@@ -516,7 +578,7 @@ Rails/RakeEnvironment:
- 'lib/tasks/i18n.rake'
- 'lib/tasks/testing.rake'
# Offense count: 8
# Offense count: 9
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: AllowedReceivers.
# AllowedReceivers: ActionMailer::Preview, ActiveSupport::TimeZone
@@ -570,6 +632,15 @@ Rails/RootPathnameMethods:
- 'lib/tasks/import.rake'
- 'spec/rails_helper.rb'
# Offense count: 4
# Configuration parameters: ForbiddenMethods, AllowedMethods.
# ForbiddenMethods: decrement!, decrement_counter, increment!, increment_counter, insert, insert!, insert_all, insert_all!, toggle!, touch, touch_all, update_all, update_attribute, update_column, update_columns, update_counters, upsert, upsert_all
Rails/SkipsModelValidations:
Exclude:
- 'db/migrate/20240101010102_populate_crop_fields_from_openfarm_data.rb'
- 'db/migrate/20240810160538_set_default_language_for_existing_alternate_names.rb'
- 'db/migrate/20251128200506_add_description_to_crops.rb'
# Offense count: 21
Rails/ThreeStateBooleanColumn:
Enabled: false
@@ -657,14 +728,6 @@ Style/FloatDivision:
Exclude:
- 'app/models/concerns/predict_planting.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: EnforcedStyle.
# SupportedStyles: always, always_true, never
Style/FrozenStringLiteralComment:
Exclude:
- 'spec/lib/haml/filters/growstuff_markdown_spec.rb'
# Offense count: 2
# This cop supports unsafe autocorrection (--autocorrect-all).
Style/GlobalStdStream:
@@ -729,6 +792,13 @@ Style/OptionalBooleanParameter:
- 'app/helpers/application_helper.rb'
- 'app/models/concerns/member_newsletter.rb'
# Offense count: 1
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
Exclude:
- 'db/migrate/20251130035700_create_versions.rb'
# Offense count: 1
# This cop supports unsafe autocorrection (--autocorrect-all).
# Configuration parameters: Methods.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -204,6 +204,15 @@ class CropsController < ApplicationController
def recreate_names(param_name, name_type)
return if params[param_name].blank?
# Get the submitted names, reject blanks, and sort for comparison
submitted_names = params[param_name].values.reject(&:blank?).sort
# Get the existing names from the database, and sort for comparison
existing_names = @crop.send("#{name_type}_names").pluck(:name).sort
# Return early to prevent destroying and recreating names (and their associated attributes)
# if the list of names has not changed.
return if submitted_names == existing_names
@crop.send("#{name_type}_names").each(&:destroy)
params[param_name].each_value do |value|
next if value.empty?
@@ -226,7 +235,7 @@ class CropsController < ApplicationController
:public_food_key,
:row_spacing, :spread, :height,
:sowing_method, :sun_requirements, :growing_degree_days,
scientific_names_attributes: %i(scientific_name _destroy id)
scientific_names_attributes: %i(name _destroy id)
)
end

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -116,11 +116,11 @@ class PlantingsController < DataController
new_planting.finished_at = nil
if new_planting.save
redirect_to edit_planting_path(new_planting), notice: t('messages.transplant_success')
redirect_to edit_planting_path(new_planting), notice: 'Planting was successfully transplanted.'
else
# if the save fails, we should probably roll back the finishing of the original planting
@planting.update(finished: false, finished_at: nil)
redirect_to @planting, alert: t('messages.transplant_error', errors: new_planting.errors.full_messages.to_sentence)
redirect_to @planting, alert: "There was an error transplanting the planting: #{new_planting.errors.full_messages.to_sentence}"
end
end

View File

@@ -29,17 +29,17 @@ class PostsController < ApplicationController
def create
params[:post][:author_id] = current_member.id
@post = Post.new(post_params)
flash[:notice] = t('posts.created') if @post.save
flash[:notice] = 'Post was successfully created.' if @post.save
respond_with(@post)
end
def update
flash[:notice] = t('posts.updated') if @post.update(post_params)
flash[:notice] = 'Post was successfully updated.' if @post.update(post_params)
respond_with(@post)
end
def destroy
flash[:notice] = t('posts.deleted') if @post.destroy
flash[:notice] = 'Post was deleted.' if @post.destroy
respond_with(@post)
end

View File

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

View File

@@ -61,7 +61,7 @@ class SeedsController < DataController
@seed.finished ||= false
@seed.owner = current_member
@seed.crop = @seed.parent_planting.crop if @seed.parent_planting
flash[:notice] = t('seeds.added_to_stash', crop: @seed.crop) if @seed.save
flash[:notice] = "Successfully added #{@seed.crop} seed to your stash." if @seed.save
if params[:return] == 'planting'
respond_with(@seed, location: @seed.parent_planting)
else
@@ -70,7 +70,7 @@ class SeedsController < DataController
end
def update
flash[:notice] = t('seeds.updated') if @seed.update(seed_params)
flash[:notice] = 'Seed was successfully updated.' if @seed.update(seed_params)
respond_with(@seed)
end

View File

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

View File

@@ -62,7 +62,7 @@ module CropsHelper
'@type': 'Person',
name: post.author.login_name
},
datePublished: post.created_at
'datePublished': post.created_at
}
end

View File

@@ -164,12 +164,6 @@ class Ability
can :destroy, Follow
cannot :destroy, Follow, followed_id: member.id # can't unfollow yourself
# blocking/unblocking permissions
can :create, Block
cannot :create, Block, blocked_id: member.id # can't block yourself
can :destroy, Block, blocker_id: member.id # can only unblock your own blocks
cannot :create, GardenType
cannot :update, GardenType
cannot :destroy, GardenType

View File

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

View File

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

View File

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

View File

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

View File

@@ -32,7 +32,7 @@ class Garden < ApplicationRecord
validates :name, uniqueness: { scope: :owner_id }
validates :name,
format: { without: /\n/, message: :no_newlines },
format: { without: /\n/, message: "must contain no newlines" },
allow_blank: false, presence: true,
length: { maximum: 255 }
@@ -53,7 +53,7 @@ class Garden < ApplicationRecord
"acres" => "acre"
}.freeze
validates :area_unit, inclusion: { in: AREA_UNITS_VALUES.values,
message: :not_a_valid_area_unit },
message: "%<value>s is not a valid area unit" },
allow_blank: true
def cleanup_area

View File

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

View File

@@ -58,18 +58,18 @@ class Harvest < ApplicationRecord
##
## Validations
validates :crop, approved: true
validates :crop, presence: { message: :crop_not_found }
validates :plant_part, presence: { message: :crop_not_found }
validates :crop, presence: { message: "must be present and exist in our database" }
validates :plant_part, presence: { message: "must be present and exist in our database" }
validates :harvested_at, presence: true
validates :quantity, allow_nil: true, numericality: {
only_integer: false, greater_than_or_equal_to: 0
}
validates :unit, allow_blank: true, inclusion: {
in: UNITS_VALUES.values, message: :not_a_valid_unit
in: UNITS_VALUES.values, message: "%<value>s is not a valid unit"
}
validates :weight_quantity, allow_nil: true, numericality: { only_integer: false }
validates :weight_unit, allow_blank: true, inclusion: {
in: WEIGHT_UNITS_VALUES.values, message: :not_a_valid_unit
in: WEIGHT_UNITS_VALUES.values, message: "%<value>s is not a valid unit"
}
validate :crop_must_match_planting
validate :owner_must_match_planting
@@ -147,7 +147,7 @@ class Harvest < ApplicationRecord
def crop_must_match_planting
return if planting.blank? # only check if we are linked to a planting
errors.add(:planting, :same_crop_required) unless crop == planting.crop
errors.add(:planting, "must be the same crop") unless crop == planting.crop
end
def owner_must_match_planting
@@ -155,13 +155,14 @@ class Harvest < ApplicationRecord
return if owner == planting.owner || planting.garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner, :same_owner_required)
errors.add(:owner,
"of harvest must be the same as planting, or a collaborator on that garden")
end
def harvest_must_be_after_planting
# only check if we are linked to a planting
return unless harvested_at.present? && planting.present? && planting.planted_at.present?
errors.add(:planting, :harvest_after_planted) unless harvested_at > planting.planted_at
errors.add(:planting, "cannot be harvested before planting") unless harvested_at > planting.planted_at
end
end

View File

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

View File

@@ -52,15 +52,6 @@ class Member < ApplicationRecord
has_many :followed, through: :follows
has_many :followers, through: :inverse_follows, source: :follower
#
# Blocking other members
has_many :blocks, class_name: "Block", foreign_key: "blocker_id", dependent: :destroy,
inverse_of: :blocker
has_many :inverse_blocks, class_name: "Block", foreign_key: "blocked_id",
dependent: :destroy, inverse_of: :blocked
has_many :blocked_members, through: :blocks, source: :blocked
has_many :blockers, through: :inverse_blocks, source: :blocker
#
# Global data records this member created
has_many :requested_crops, class_name: 'Crop', foreign_key: 'requester_id', dependent: :nullify,
@@ -105,21 +96,21 @@ class Member < ApplicationRecord
validates :tos_agreement, acceptance: { allow_nil: true, accept: true }
validates :login_name,
length: {
minimum: 2, maximum: 25, message: :login_name_length
minimum: 2, maximum: 25, message: "should be between 2 and 25 characters long"
},
exclusion: {
in: %w(growstuff admin moderator staff nearby), message: :login_name_reserved
in: %w(growstuff admin moderator staff nearby), message: "name is reserved"
},
format: {
with: /\A\w+\z/, message: :login_name_format
with: /\A\w+\z/, message: "may only include letters, numbers, or underscores"
},
uniqueness: {
case_sensitive: false
}
validates :website_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
validates :other_url, format: { with: %r{\Ahttps?://}, message: :url_format }, allow_blank: true
validates :website_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }, allow_blank: true
validates :other_url, format: { with: %r{\Ahttps?://}, message: "must start with http:// or https://" }, allow_blank: true
validates :instagram_handle, :facebook_handle, :bluesky_handle,
format: { without: %r{\Ahttps?://|/}, message: :handle_format }, allow_blank: true
format: { without: %r{\Ahttps?://|/}, message: "should be a handle, not a URL" }, allow_blank: true
#
# Triggers
@@ -188,12 +179,4 @@ class Member < ApplicationRecord
def get_follow(member)
follows.find_by(followed_id: member.id) if already_following?(member)
end
def already_blocking?(member)
blocks.exists?(blocked_id: member.id)
end
def get_block(member)
blocks.find_by(blocked_id: member.id) if already_blocking?(member)
end
end

View File

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

View File

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

View File

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

View File

@@ -16,7 +16,7 @@
%p
%span.help-block
For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
= link_to "crop wrangling guide", "http://wiki.growstuff.org/index.php/Crop_wrangling"
on the Growstuff wiki.
.form-group

View File

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

View File

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

View File

@@ -11,7 +11,7 @@
%p
%span.help-block
For detailed crop wrangling guidelines, please consult the
= link_to "crop wrangling guide", "https://github.com/Growstuff/growstuff/wiki/Crop-Wrangling"
= link_to "crop wrangling guide", "http://wiki.growstuff.org/index.php/Crop_wrangling"
on the Growstuff wiki.
.form-group

24
config.rb Normal file
View File

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

4
config/compass.rb Normal file
View File

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

View File

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

View File

@@ -105,7 +105,6 @@ Rails.application.routes.draw do
resources :forums
resources :follows, only: %i(create destroy)
resources :blocks, only: %i(create destroy)
post 'likes' => 'likes#create'
delete 'likes' => 'likes#destroy'
@@ -122,7 +121,6 @@ Rails.application.routes.draw do
resources :follows
get 'followers' => 'follows#followers'
resources :blocks, only: %i(create destroy)
end
resources :messages

View File

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

View File

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

View File

@@ -384,16 +384,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
t.index ["member_id"], name: "index_authentications_on_member_id"
end
create_table "blocks", force: :cascade do |t|
t.bigint "blocker_id"
t.bigint "blocked_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["blocked_id"], name: "index_blocks_on_blocked_id"
t.index ["blocker_id", "blocked_id"], name: "index_blocks_on_blocker_id_and_blocked_id", unique: true
t.index ["blocker_id"], name: "index_blocks_on_blocker_id"
end
create_table "comfy_cms_categories", id: :serial, force: :cascade do |t|
t.integer "site_id", null: false
t.string "label", null: false
@@ -982,8 +972,6 @@ ActiveRecord::Schema[7.2].define(version: 2025_12_01_045000) do
add_foreign_key "active_storage_attachments", "active_storage_blobs", column: "blob_id"
add_foreign_key "active_storage_variant_records", "active_storage_blobs", column: "blob_id"
add_foreign_key "blocks", "members", column: "blocked_id"
add_foreign_key "blocks", "members", column: "blocker_id"
add_foreign_key "harvests", "plantings"
add_foreign_key "mailboxer_conversation_opt_outs", "mailboxer_conversations", column: "conversation_id", name: "mb_opt_outs_on_conversations_id"
add_foreign_key "mailboxer_notifications", "mailboxer_conversations", column: "conversation_id", name: "notifications_on_conversation_id"

View File

@@ -1,76 +0,0 @@
version: '3.8'
services:
web:
build: .
ports:
- "3000:3000"
depends_on:
db:
condition: service_healthy
elasticsearch:
condition: service_healthy
environment:
DATABASE_URL: postgresql://postgres:postgres@db:5432/growstuff_prod
ELASTICSEARCH_URL: http://elasticsearch:9200/
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "true"
RAILS_SERVE_STATIC_FILES: "true"
APP_DOMAIN_NAME: localhost:3000
APP_PROTOCOL: http
DEVISE_SECRET_KEY: secret
GROWSTUFF_EMAIL: "noreply@test.growstuff.org"
GROWSTUFF_FLICKR_KEY: secretkey
GROWSTUFF_FLICKR_SECRET: secretsecret
GROWSTUFF_SITE_NAME: "Growstuff (local)"
RAILS_SECRET_TOKEN: supersecret
SECRET_KEY_BASE: supersecretbase
db:
image: postgres:17
restart: unless-stopped
volumes:
- postgres-data:/var/lib/postgresql/data
- .devcontainer/create-db-user.sql:/docker-entrypoint-initdb.d/create-db-user.sql
environment:
POSTGRES_USER: postgres
POSTGRES_DB: growstuff_prod
POSTGRES_PASSWORD: postgres
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready"]
interval: 10s
timeout: 5s
retries: 5
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:7.4.0
container_name: elasticsearch
restart: unless-stopped
environment:
- xpack.security.enabled=false
- discovery.type=single-node
ulimits:
memlock:
soft: -1
hard: -1
nofile:
soft: 65536
hard: 65536
cap_add:
- IPC_LOCK
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:9200 | grep tagline"]
interval: 10s
timeout: 10s
retries: 120
volumes:
- esdata01:/usr/share/elasticsearch/data
ports:
- 9200:9200
- 9300:9300
volumes:
postgres-data:
esdata01:

View File

@@ -1,8 +0,0 @@
#!/bin/bash
set -e
# Remove a potentially pre-existing server.pid for Rails.
rm -f /app/tmp/pids/server.pid
# Then exec the container's main process (what's set as CMD in the Dockerfile).
exec "$@"

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
require 'rails_helper'
describe AuthenticationsController do
before do
@member = create(:member)
sign_in @member
controller.stub(:current_member) { @member }
@auth = create(:authentication, member: @member)
request.env['omniauth.auth'] = {
'provider' => 'foo',
'uid' => 'bar',
'info' => { 'nickname' => 'blah' },
'credentials' => { 'token' => 'blah', 'secret' => 'blah' }
}
end
end

View File

@@ -3,111 +3,17 @@
require 'rails_helper'
describe ForumsController do
let(:admin) { create(:admin_member) }
let(:member) { create(:member) }
let(:forum) { create(:forum) }
login_member(:admin_member)
describe "GET #index" do
it "returns a success response" do
get :index
expect(response).to be_successful
end
it "assigns @forums" do
forum # create forum
get :index
expect(assigns(:forums)).to include(forum)
end
def valid_attributes
{
"name" => "MyString",
"description" => "Something",
"owner_id" => 1
}
end
describe "GET #show" do
it "returns a success response" do
get :show, params: { id: forum.to_param }
expect(response).to be_successful
end
end
context "as an admin" do
before { sign_in admin }
describe "GET #new" do
it "returns a success response" do
get :new
expect(response).to be_successful
end
end
describe "GET #edit" do
it "returns a success response" do
get :edit, params: { id: forum.to_param }
expect(response).to be_successful
end
end
describe "POST #create" do
context "with valid params" do
let(:valid_attributes) { { name: "New Forum", description: "A new forum", owner_id: admin.id } }
it "creates a new Forum" do
expect {
post :create, params: { forum: valid_attributes }
}.to change(Forum, :count).by(1)
end
it "redirects to the created forum" do
post :create, params: { forum: valid_attributes }
expect(response).to redirect_to(Forum.last)
end
end
end
describe "PUT #update" do
context "with valid params" do
let(:new_attributes) { { name: "Updated Name" } }
it "updates the requested forum" do
put :update, params: { id: forum.to_param, forum: new_attributes }
forum.reload
expect(forum.name).to eq("Updated Name")
end
it "redirects to the forum" do
put :update, params: { id: forum.to_param, forum: new_attributes }
expect(response).to redirect_to(forum)
end
end
end
describe "DELETE #destroy" do
it "destroys the requested forum" do
forum # ensure forum exists
expect {
delete :destroy, params: { id: forum.to_param }
}.to change(Forum, :count).by(-1)
end
it "redirects to the forums list" do
delete :destroy, params: { id: forum.to_param }
expect(response).to redirect_to(forums_url)
end
end
end
context "as a regular member" do
before { sign_in member }
describe "GET #new" do
it "denies access" do
get :new
expect(response).to redirect_to(root_path)
end
end
describe "POST #create" do
it "denies access" do
post :create, params: { forum: { name: "Forbidden" } }
expect(response).to redirect_to(root_path)
end
end
def valid_session
{}
end
end

View File

@@ -24,42 +24,29 @@ RSpec.describe GardenTypesController do
describe 'changing existing records' do
before do
allow(GardenType).to receive(:find).and_return(:garden_type)
expect(garden_type).not_to receive(:save)
expect(garden_type).not_to receive(:save!)
expect(garden_type).not_to receive(:update)
expect(garden_type).not_to receive(:update!)
expect(garden_type).not_to receive(:destroy)
end
describe 'GET edit' do
it "redirects to root" do
expect(garden_type).not_to receive(:save)
expect(garden_type).not_to receive(:save!)
expect(garden_type).not_to receive(:update)
expect(garden_type).not_to receive(:update!)
expect(garden_type).not_to receive(:destroy)
get :edit, params: { id: garden_type.to_param }
expect(response).to redirect_to(root_path)
end
before { get :edit, params: { id: garden_type.to_param } }
it { expect(response).to redirect_to(root_path) }
end
describe 'POST update' do
it "redirects to root" do
expect(garden_type).not_to receive(:save)
expect(garden_type).not_to receive(:save!)
expect(garden_type).not_to receive(:update)
expect(garden_type).not_to receive(:update!)
expect(garden_type).not_to receive(:destroy)
post :update, params: { id: garden_type.to_param, garden_type: valid_params }
expect(response).to redirect_to(root_path)
end
before { post :update, params: { id: garden_type.to_param, garden_type: valid_params } }
it { expect(response).to redirect_to(root_path) }
end
describe 'DELETE' do
it "redirects to root" do
expect(garden_type).not_to receive(:save)
expect(garden_type).not_to receive(:save!)
expect(garden_type).not_to receive(:update)
expect(garden_type).not_to receive(:update!)
expect(garden_type).not_to receive(:destroy)
delete :destroy, params: { id: garden_type.to_param, params: { garden_type: valid_params } }
expect(response).to redirect_to(root_path)
end
before { delete :destroy, params: { id: garden_type.to_param, params: { garden_type: valid_params } } }
it { expect(response).to redirect_to(root_path) }
end
end
end
@@ -73,43 +60,30 @@ RSpec.describe GardenTypesController do
let(:any_garden_type) { double('garden_type') }
before do
allow(GardenType).to receive(:find).and_return(:any_garden_type)
expect(GardenType).to receive(:find).and_return(:any_garden_type)
expect(any_garden_type).not_to receive(:save)
expect(any_garden_type).not_to receive(:save!)
expect(any_garden_type).not_to receive(:update)
expect(any_garden_type).not_to receive(:update!)
expect(any_garden_type).not_to receive(:destroy)
end
describe 'GET edit' do
it "redirects to root" do
expect(any_garden_type).not_to receive(:save)
expect(any_garden_type).not_to receive(:save!)
expect(any_garden_type).not_to receive(:update)
expect(any_garden_type).not_to receive(:update!)
expect(any_garden_type).not_to receive(:destroy)
get :edit, params: { id: any_garden_type.to_param }
expect(response).to redirect_to(root_path)
end
before { get :edit, params: { id: any_garden_type.to_param } }
it { expect(response).to redirect_to(root_path) }
end
describe 'POST update' do
it "redirects to root" do
expect(any_garden_type).not_to receive(:save)
expect(any_garden_type).not_to receive(:save!)
expect(any_garden_type).not_to receive(:update)
expect(any_garden_type).not_to receive(:update!)
expect(any_garden_type).not_to receive(:destroy)
post :update, params: { id: any_garden_type.to_param, garden_type: valid_params }
expect(response).to redirect_to(root_path)
end
before { post :update, params: { id: any_garden_type.to_param, garden_type: valid_params } }
it { expect(response).to redirect_to(root_path) }
end
describe 'DELETE' do
it "redirects to root" do
expect(any_garden_type).not_to receive(:save)
expect(any_garden_type).not_to receive(:save!)
expect(any_garden_type).not_to receive(:update)
expect(any_garden_type).not_to receive(:update!)
expect(any_garden_type).not_to receive(:destroy)
delete :destroy, params: { id: any_garden_type.to_param, params: { garden_type: valid_params } }
expect(response).to redirect_to(root_path)
end
before { delete :destroy, params: { id: any_garden_type.to_param, params: { garden_type: valid_params } } }
it { expect(response).to redirect_to(root_path) }
end
end
end

View File

@@ -25,42 +25,29 @@ RSpec.describe GardensController do
describe 'changing existing records' do
before do
allow(Garden).to receive(:find).and_return(:garden)
expect(garden).not_to receive(:save)
expect(garden).not_to receive(:save!)
expect(garden).not_to receive(:update)
expect(garden).not_to receive(:update!)
expect(garden).not_to receive(:destroy)
end
describe 'GET edit' do
it "redirects to login" do
expect(garden).not_to receive(:save)
expect(garden).not_to receive(:save!)
expect(garden).not_to receive(:update)
expect(garden).not_to receive(:update!)
expect(garden).not_to receive(:destroy)
get :edit, params: { slug: garden.to_param }
expect(response).to redirect_to(new_member_session_path)
end
before { get :edit, params: { slug: garden.to_param } }
it { expect(response).to redirect_to(new_member_session_path) }
end
describe 'POST update' do
it "redirects to login" do
expect(garden).not_to receive(:save)
expect(garden).not_to receive(:save!)
expect(garden).not_to receive(:update)
expect(garden).not_to receive(:update!)
expect(garden).not_to receive(:destroy)
post :update, params: { slug: garden.to_param, garden: valid_params }
expect(response).to redirect_to(new_member_session_path)
end
before { post :update, params: { slug: garden.to_param, garden: valid_params } }
it { expect(response).to redirect_to(new_member_session_path) }
end
describe 'DELETE' do
it "redirects to login" do
expect(garden).not_to receive(:save)
expect(garden).not_to receive(:save!)
expect(garden).not_to receive(:update)
expect(garden).not_to receive(:update!)
expect(garden).not_to receive(:destroy)
delete :destroy, params: { slug: garden.to_param, params: { garden: valid_params } }
expect(response).to redirect_to(new_member_session_path)
end
before { delete :destroy, params: { slug: garden.to_param, params: { garden: valid_params } } }
it { expect(response).to redirect_to(new_member_session_path) }
end
end
end
@@ -74,43 +61,30 @@ RSpec.describe GardensController do
let(:not_my_garden) { double('garden') }
before do
allow(Garden).to receive(:find).and_return(:not_my_garden)
expect(Garden).to receive(:find).and_return(:not_my_garden)
expect(not_my_garden).not_to receive(:save)
expect(not_my_garden).not_to receive(:save!)
expect(not_my_garden).not_to receive(:update)
expect(not_my_garden).not_to receive(:update!)
expect(not_my_garden).not_to receive(:destroy)
end
describe 'GET edit' do
it "redirects to root" do
expect(not_my_garden).not_to receive(:save)
expect(not_my_garden).not_to receive(:save!)
expect(not_my_garden).not_to receive(:update)
expect(not_my_garden).not_to receive(:update!)
expect(not_my_garden).not_to receive(:destroy)
get :edit, params: { slug: not_my_garden.to_param }
expect(response).to redirect_to(root_path)
end
before { get :edit, params: { slug: not_my_garden.to_param } }
it { expect(response).to redirect_to(root_path) }
end
describe 'POST update' do
it "redirects to root" do
expect(not_my_garden).not_to receive(:save)
expect(not_my_garden).not_to receive(:save!)
expect(not_my_garden).not_to receive(:update)
expect(not_my_garden).not_to receive(:update!)
expect(not_my_garden).not_to receive(:destroy)
post :update, params: { slug: not_my_garden.to_param, garden: valid_params }
expect(response).to redirect_to(root_path)
end
before { post :update, params: { slug: not_my_garden.to_param, garden: valid_params } }
it { expect(response).to redirect_to(root_path) }
end
describe 'DELETE' do
it "redirects to root" do
expect(not_my_garden).not_to receive(:save)
expect(not_my_garden).not_to receive(:save!)
expect(not_my_garden).not_to receive(:update)
expect(not_my_garden).not_to receive(:update!)
expect(not_my_garden).not_to receive(:destroy)
delete :destroy, params: { slug: not_my_garden.to_param, params: { garden: valid_params } }
expect(response).to redirect_to(root_path)
end
before { delete :destroy, params: { slug: not_my_garden.to_param, params: { garden: valid_params } } }
it { expect(response).to redirect_to(root_path) }
end
end
end

View File

@@ -15,12 +15,12 @@ describe HarvestsController, :search do
end
describe "GET index" do
let!(:first_member) { create(:member) }
let(:second_member) { create(:member) }
let(:tomato) { create(:tomato) }
let(:maize) { create(:maize) }
let!(:tomato_harvest) { create(:harvest, owner_id: first_member.id, crop_id: tomato.id) }
let!(:maize_harvest) { create(:harvest, owner_id: second_member.id, crop_id: maize.id) }
let!(:member1) { create(:member) }
let(:member2) { create(:member) }
let(:tomato) { create(:tomato) }
let(:maize) { create(:maize) }
let!(:harvest1) { create(:harvest, owner_id: member1.id, crop_id: tomato.id) }
let!(:harvest2) { create(:harvest, owner_id: member2.id, crop_id: maize.id) }
before { Harvest.reindex }
@@ -28,16 +28,16 @@ describe HarvestsController, :search do
before { get :index, params: {} }
it { expect(assigns(:harvests).size).to eq 2 }
it { expect(assigns(:harvests)[0].slug).to eq tomato_harvest.slug }
it { expect(assigns(:harvests)[1].slug).to eq maize_harvest.slug }
it { expect(assigns(:harvests)[0].slug).to eq harvest1.slug }
it { expect(assigns(:harvests)[1].slug).to eq harvest2.slug }
end
describe "picks up owner from params and shows owner's harvests only" do
before { get :index, params: { member_slug: first_member.slug } }
before { get :index, params: { member_slug: member1.slug } }
it { expect(assigns(:owner)).to eq first_member }
it { expect(assigns(:owner)).to eq member1 }
it { expect(assigns(:harvests).size).to eq 1 }
it { expect(assigns(:harvests)[0].slug).to eq tomato_harvest.slug }
it { expect(assigns(:harvests)[0].slug).to eq harvest1.slug }
end
describe "picks up crop from params and shows the harvests for the crop only" do
@@ -45,7 +45,7 @@ describe HarvestsController, :search do
it { expect(assigns(:crop)).to eq maize }
it { expect(assigns(:harvests).size).to eq 1 }
it { expect(assigns(:harvests)[0].slug).to eq maize_harvest.slug }
it { expect(assigns(:harvests)[0].slug).to eq harvest2.slug }
end
describe "generates a csv" do

View File

@@ -13,12 +13,12 @@ describe PlantingsController, :search do
end
describe "GET index", :search do
let!(:first_member) { create(:member) }
let!(:second_member) { create(:member) }
let!(:tomato) { create(:tomato) }
let!(:maize) { create(:maize) }
let!(:tomato_planting) { create(:planting, crop: tomato, owner: first_member, created_at: 1.day.ago) }
let!(:maize_planting) { create(:planting, crop: maize, owner: second_member, created_at: 5.days.ago) }
let!(:member1) { create(:member) }
let!(:member2) { create(:member) }
let!(:tomato) { create(:tomato) }
let!(:maize) { create(:maize) }
let!(:planting1) { create(:planting, crop: tomato, owner: member1, created_at: 1.day.ago) }
let!(:planting2) { create(:planting, crop: maize, owner: member2, created_at: 5.days.ago) }
before do
Planting.reindex
@@ -28,23 +28,23 @@ describe PlantingsController, :search do
before { get :index }
it { expect(assigns(:plantings).size).to eq 2 }
it { expect(assigns(:plantings)[0]['slug']).to eq tomato_planting.slug }
it { expect(assigns(:plantings)[1]['slug']).to eq maize_planting.slug }
it { expect(assigns(:plantings)[0]['slug']).to eq planting1.slug }
it { expect(assigns(:plantings)[1]['slug']).to eq planting2.slug }
end
describe "picks up owner from params and shows owner's plantings only" do
before { get :index, params: { member_slug: first_member.slug } }
before { get :index, params: { member_slug: member1.slug } }
it { expect(assigns(:owner)).to eq first_member }
it { expect(assigns(:owner)).to eq member1 }
it { expect(assigns(:plantings).size).to eq 1 }
it { expect(assigns(:plantings).first['slug']).to eq tomato_planting.slug }
it { expect(assigns(:plantings).first['slug']).to eq planting1.slug }
end
describe "picks up crop from params and shows the plantings for the crop only" do
before { get :index, params: { crop_slug: maize.slug } }
it { expect(assigns(:crop)).to eq maize }
it { expect(assigns(:plantings).first['slug']).to eq maize_planting.slug }
it { expect(assigns(:plantings).first['slug']).to eq planting2.slug }
end
end

View File

@@ -21,6 +21,10 @@ describe SeedsController, :search do
it { expect(response).to be_successful }
context 'no parent planting' do
before { get :new }
end
context 'with parent planting' do
let!(:planting) { create(:planting, owner:) }

View File

@@ -22,6 +22,7 @@ describe "forums", :js do
before do
visit forums_path
click_link "New forum"
expect(page).to have_current_path new_forum_path, ignore_query: true
fill_in 'Name', with: 'Discussion'
fill_in 'Description', with: "this is a new forum"
select member.login_name, from: "Owner"

View File

@@ -23,6 +23,7 @@ describe "plant parts", :js do
before do
visit plant_parts_path
click_link "New plant part"
expect(page).to have_current_path new_plant_part_path, ignore_query: true
fill_in 'Name', with: "this is a new plant part"
click_button 'Save'
end

View File

@@ -23,6 +23,7 @@ describe "roles", :js do
before do
visit admin_roles_path
click_link "New role"
expect(page).to have_current_path new_admin_role_path, ignore_query: true
fill_in 'Name', with: 'Discussion'
fill_in 'Description', with: "this is a new role"
click_button 'Save'

View File

@@ -15,44 +15,41 @@ describe "crop detail page", :js, :search do
let!(:planting) { create(:planting, crop:, owner: owner_member) }
let!(:seed) { create(:seed, crop:, owner: owner_member) }
let!(:first_planting_photo) { create(:photo, owner: owner_member) }
let!(:second_planting_photo) { create(:photo, owner: owner_member) }
let!(:first_harvest_photo) { create(:photo, owner: owner_member) }
let!(:second_harvest_photo) { create(:photo, owner: owner_member) }
let!(:first_seed_photo) { create(:photo, owner: owner_member) }
let!(:second_seed_photo) { create(:photo, owner: owner_member) }
let!(:photo1) { create(:photo, owner: owner_member) }
let!(:photo2) { create(:photo, owner: owner_member) }
let!(:photo3) { create(:photo, owner: owner_member) }
let!(:photo4) { create(:photo, owner: owner_member) }
let!(:photo5) { create(:photo, owner: owner_member) }
let!(:photo6) { create(:photo, owner: owner_member) }
before do
planting.photos << first_planting_photo
planting.photos << second_planting_photo
harvest.photos << first_harvest_photo
harvest.photos << second_harvest_photo
seed.photos << first_seed_photo
seed.photos << second_seed_photo
planting.photos << photo1
planting.photos << photo2
harvest.photos << photo3
harvest.photos << photo4
seed.photos << photo5
seed.photos << photo6
Crop.reindex
visit crop_path(crop)
expect(crop.photos.count).to eq 6
expect(crop.photos.by_model(Planting).count).to eq 2
expect(page).to have_content 'Photos'
end
shared_examples "shows photos" do
it "shows the photo section" do
expect(crop.photos.count).to eq 6
expect(crop.photos.by_model(Planting).count).to eq 2
expect(page).to have_content 'Photos'
end
describe "show planting photos" do
it { is_expected.to have_xpath("//img[contains(@src,'#{first_planting_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{second_planting_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo1.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo2.fullsize_url}')]") }
end
describe "show harvest photos" do
it { is_expected.to have_xpath("//img[contains(@src,'#{first_harvest_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{second_harvest_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo3.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo4.fullsize_url}')]") }
end
describe "show seed photos" do
it { is_expected.to have_xpath("//img[contains(@src,'#{first_seed_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{second_seed_photo.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo5.fullsize_url}')]") }
it { is_expected.to have_xpath("//img[contains(@src,'#{photo6.fullsize_url}')]") }
end
describe "link to more photos" do

View File

@@ -1,47 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe "Forums usage", :js do
let!(:forum) { create(:forum, name: "General Discussion", description: "Talk about anything") }
let(:member) { create(:member) }
describe "browsing forums" do
it "shows the list of forums" do
visit forums_path
expect(page).to have_content("General Discussion")
expect(page).to have_content("Talk about anything")
end
end
describe "viewing a forum" do
let!(:post) { create(:post, forum: forum, subject: "Hello World", author: member) }
it "shows forum details and posts" do
visit forum_path(forum)
expect(page).to have_css("h1", text: "General Discussion")
expect(page).to have_content("Talk about anything")
expect(page).to have_content("Hello World")
expect(page).to have_link("Post something")
end
end
describe "starting a new post from a forum" do
include_context 'signed in member'
it "pre-fills the forum when creating a new post" do
visit forum_path(forum)
click_link "Post something"
expect(page).to have_current_path(new_post_path(forum_id: forum.id))
expect(page).to have_content("This post will be posted in the forum #{forum.name}")
fill_in "post_subject", with: "My New Post"
fill_in "post_body", with: "Content of my post"
click_button "Post"
expect(page).to have_content("Post was successfully created")
expect(Post.last.forum).to eq(forum)
end
end
end

View File

@@ -1,72 +0,0 @@
# frozen_string_literal: true
require 'rails_helper'
describe "blocks", :js do
context "when signed in" do
include_context 'signed in member'
let(:other_member) { create(:member) }
it "your profile doesn't have a block button" do
visit member_path(member)
expect(page).to have_no_link "Block"
expect(page).to have_no_link "Unblock"
end
context "blocking another member" do
before { visit member_path(other_member) }
it "has a block button" do
expect(page).to have_link "Block", href: blocks_path(blocked: other_member.slug)
end
it "has correct message and unblock button" do
click_link 'Block'
expect(page).to have_content "Blocked #{other_member.login_name}"
expect(page).to have_link "Unblock", href: block_path(member.get_block(other_member))
end
it "has correct message and block button after unblock" do
click_link 'Block'
click_link 'Unblock'
expect(page).to have_content "Unblocked #{other_member.login_name}"
visit member_path(other_member) # unblocking redirects to root
expect(page).to have_link "Block", href: blocks_path(blocked: other_member.slug)
end
context "when a member is blocked" do
before do
click_link 'Block'
end
it "prevents following" do
visit member_path(other_member)
expect(page).to have_no_link "Follow"
end
xit "prevents messaging" do
visit new_message_path(recipient_id: other_member.id)
fill_in "Subject", with: "Test message"
fill_in "Body", with: "Test message body"
click_button "Send message"
expect(page).to have_content "You cannot send a message to a member who has blocked you."
end
xit "prevents commenting" do
post = create(:post, author: other_member)
visit post_path(post)
fill_in "comment_body", with: "Test comment"
click_button "Post Comment"
expect(page).to have_content "You cannot comment on a post of a member who has blocked you."
end
xit "prevents liking" do
post = create(:post, author: other_member)
visit post_path(post)
click_link "Like"
expect(page).to have_content "You cannot like content of a member who has blocked you."
end
end
end
end
end

View File

@@ -23,17 +23,6 @@ describe "follows", :js do
expect(page).to have_no_link "Unfollow"
end
context "when the other member is blocked" do
before do
member.blocks.create(blocked: other_member)
visit member_path(other_member)
end
it "does not have a follow button" do
expect(page).to have_no_link "Follow"
end
end
context "following another member" do
before { visit member_path(other_member) }

View File

@@ -6,29 +6,27 @@ describe "members list" do
context "list all members" do
subject { page.all("#maincontainer h4.login-name") }
let!(:archaeopteryx) { create(:member, login_name: "Archaeopteryx", confirmed_at: Time.zone.parse('2013-02-10')) }
let!(:zephyrosaurus) { create(:member, login_name: "Zephyrosaurus", confirmed_at: Time.zone.parse('2014-01-11')) }
let!(:testingname) { create(:member, login_name: "Testingname", confirmed_at: Time.zone.parse('2014-05-09')) }
let!(:member1) { create(:member, login_name: "Archaeopteryx", confirmed_at: Time.zone.parse('2013-02-10')) }
let!(:member2) { create(:member, login_name: "Zephyrosaurus", confirmed_at: Time.zone.parse('2014-01-11')) }
let!(:member3) { create(:member, login_name: "Testingname", confirmed_at: Time.zone.parse('2014-05-09')) }
before do
visit members_path
expect(page).to have_css "#sort"
expect(page).to have_css "form"
end
it "default alphabetical sort" do
expect(page).to have_css "#sort"
expect(page).to have_css "form"
click_button('Show')
expect(subject.first).to have_text archaeopteryx.login_name
expect(subject.last).to have_text zephyrosaurus.login_name
expect(subject.first).to have_text member1.login_name
expect(subject.last).to have_text member2.login_name
end
it "recently joined sort" do
expect(page).to have_css "#sort"
expect(page).to have_css "form"
select("recently", from: 'sort')
click_button('Show')
expect(subject.first).to have_text testingname.login_name
expect(subject.last).to have_text archaeopteryx.login_name
expect(subject.first).to have_text member3.login_name
expect(subject.last).to have_text member1.login_name
end
end
end

View File

@@ -118,15 +118,15 @@ describe "member profile", :js do
end
context 'member has activities' do
let!(:past_activity) { create(:activity, owner: member, due_date: 3.days.ago) }
let!(:planting_activity) { create(:activity, :planting, owner: member) }
let!(:garden_activity) { create(:activity, :garden, owner: member) }
let!(:activity) { create(:activity, owner: member, due_date: 3.days.ago) }
let!(:activity2) { create(:activity, :planting, owner: member) }
let!(:activity3) { create(:activity, :garden, owner: member) }
before { visit member_path(member) }
it { expect(page).to have_link href: activity_path(past_activity) }
it { expect(page).to have_link href: activity_path(planting_activity) }
it { expect(page).to have_link href: activity_path(garden_activity) }
it { expect(page).to have_link href: activity_path(activity) }
it { expect(page).to have_link href: activity_path(activity2) }
it { expect(page).to have_link href: activity_path(activity3) }
end
context 'member has seeds' do

View File

@@ -7,17 +7,17 @@ describe 'Test with visual testing', :js do
# on every run, so doesn't trigger percy to see changes
before { Faker::Config.random = Random.new(42) }
let!(:member) { create(:member, login_name: 'percy', preferred_avatar_uri: member_gravatar) }
let!(:crop_wrangler) { create(:crop_wrangling_member, login_name: 'croppy', preferred_avatar_uri: crop_wrangler_gravatar) }
let!(:admin_user) { create(:admin_member, login_name: 'janitor', preferred_avatar_uri: admin_gravatar) }
let!(:someone_else) { create(:edinburgh_member, login_name: 'ruby', preferred_avatar_uri: someone_else_gravatar) }
let!(:member) { create(:member, login_name: 'percy', preferred_avatar_uri: gravatar) }
let!(:crop_wrangler) { create(:crop_wrangling_member, login_name: 'croppy', preferred_avatar_uri: gravatar2) }
let!(:admin_user) { create(:admin_member, login_name: 'janitor', preferred_avatar_uri: gravatar3) }
let!(:someone_else) { create(:edinburgh_member, login_name: 'ruby', preferred_avatar_uri: gravatar4) }
let(:member_gravatar) { 'https://secure.gravatar.com/avatar/d021434aac03a7f7c7c0de60d07dad1c?size=150&default=identicon' }
let(:crop_wrangler_gravatar) { 'https://secure.gravatar.com/avatar/353d83d3677b142520987e1936fd093c?size=150&default=identicon' }
let(:admin_gravatar) { 'https://secure.gravatar.com/avatar/622db62c7beab8d5d8b7a80aa6385b2f?size=150&default=identicon' }
let(:someone_else_gravatar) { 'https://secure.gravatar.com/avatar/7fd767571ff5ceefc7a687a543b2c402?size=150&default=identicon' }
let(:gravatar) { 'https://secure.gravatar.com/avatar/d021434aac03a7f7c7c0de60d07dad1c?size=150&default=identicon' }
let(:gravatar2) { 'https://secure.gravatar.com/avatar/353d83d3677b142520987e1936fd093c?size=150&default=identicon' }
let(:gravatar3) { 'https://secure.gravatar.com/avatar/622db62c7beab8d5d8b7a80aa6385b2f?size=150&default=identicon' }
let(:gravatar4) { 'https://secure.gravatar.com/avatar/7fd767571ff5ceefc7a687a543b2c402?size=150&default=identicon' }
let!(:tomato) { create(:tomato, creator: someone_else) }
let!(:tomato) { create(:tomato, creator: someone_else) }
let(:plant_part) { create(:plant_part, name: 'fruit') }
let(:tomato_photo) do

View File

@@ -30,13 +30,13 @@ describe "Planting reminder email", :js do
context "when member has some plantings" do
# Bangs are used on the following 2 let blocks in order to ensure that the plantings are present
# in the database before the email is generated: otherwise, they won't be present in the email.
let!(:recent_planting) { create(:predictable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) }
let!(:older_planting) { create(:predictable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) }
let!(:p1) { create(:predictable_planting, planted_at: 10.days.ago, garden: member.gardens.first, owner: member) }
let!(:p2) { create(:predictable_planting, planted_at: 30.days.ago, garden: member.gardens.first, owner: member) }
describe "lists plantings" do
it { expect(mail).to have_content "Progress report" }
it { expect(mail).to have_link recent_planting.crop.to_s, href: planting_url(recent_planting) }
it { expect(mail).to have_link older_planting.crop.to_s, href: planting_url(older_planting) }
it { expect(mail).to have_link p1.crop.to_s, href: planting_url(p1) }
it { expect(mail).to have_link p2.crop.to_s, href: planting_url(p2) }
it { expect(mail).to have_content "keep your garden records up to date" }
end
end
@@ -50,15 +50,15 @@ describe "Planting reminder email", :js do
context "when member has some harvests" do
# Bangs are used on the following 2 let blocks in order to ensure that the plantings are present
# in the database before the spec is run.
let!(:recent_planting) { create(:predictable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) }
let!(:older_planting) { create(:predictable_planting, garden: member.gardens.first, owner: member) }
let!(:recent_harvest) { create(:harvest, owner: member, planting: recent_planting, harvested_at: 1.day.ago) }
let!(:older_harvest) { create(:harvest, owner: member, planting: older_planting, harvested_at: 3.days.ago) }
let!(:p1) { create(:predictable_planting, garden: member.gardens.first, owner: member, planted_at: 20.days.ago) }
let!(:p2) { create(:predictable_planting, garden: member.gardens.first, owner: member) }
let!(:h1) { create(:harvest, owner: member, planting: p1, harvested_at: 1.day.ago) }
let!(:h2) { create(:harvest, owner: member, planting: p2, harvested_at: 3.days.ago) }
describe "lists planting that are ready for harvest" do
it { expect(mail).to have_content "Ready to harvest" }
it { expect(mail).to have_link recent_planting.crop.name, href: planting_url(recent_planting) }
it { expect(mail).to have_link older_planting.crop.name, href: planting_url(older_planting) }
it { expect(mail).to have_link p1.crop.name, href: planting_url(p1) }
it { expect(mail).to have_link p2.crop.name, href: planting_url(p2) }
it { expect(mail).to have_content "Harvested anything lately?" }
end
end

View File

@@ -286,9 +286,9 @@ describe "Planting a crop", :js, :search do
check "Mark as finished"
click_button "Save"
end
end
xit { expect(page).to have_css("img[alt='sun']") }
it { expect(page).to have_css("img[alt='sun']") }
end
end
describe "Marking a planting as finished from the show page" do

View File

@@ -11,7 +11,7 @@ shared_examples "append date" do
click_link 'Actions'
click_link link_text
within "div.datepicker" do
find(".datepicker-days", text: this_month.to_s)
expect(page).to have_content this_month.to_s
find(".datepicker-days td.day", text: "21").click
end
end

View File

@@ -4,23 +4,23 @@ require 'rails_helper'
describe "timeline", :js do
let(:member) { create(:member) }
let(:planting_friend) { create(:member) }
let(:post_friend) { create(:member) }
let(:friend1) { create(:member) }
let(:friend2) { create(:member) }
before do
member.followed << planting_friend
member.followed << post_friend
member.followed << friend1
member.followed << friend2
end
describe 'visit timeline' do
let!(:friend_planting) { create(:planting, owner: planting_friend, planted_at: 1.day.ago) }
let!(:friend_harvest) { create(:planting, owner: post_friend, planted_at: 3.years.ago) }
let!(:finished_planting) { create(:finished_planting, owner: planting_friend) }
let!(:no_planted_at_planting) { create(:planting, owner: post_friend, planted_at: nil) }
let!(:friend_photo) { create(:photo, owner: planting_friend) }
let!(:friend_post) { create(:post, author: post_friend) }
let!(:liked_post) { create(:like, likeable: friend_photo, member: post_friend) }
let!(:liked_photo) { create(:like, likeable: friend_post, member: planting_friend) }
let!(:friend_planting) { create(:planting, owner: friend1, planted_at: 1.day.ago) }
let!(:friend_harvest) { create(:planting, owner: friend2, planted_at: 3.years.ago) }
let!(:finished_planting) { create(:finished_planting, owner: friend1) }
let!(:no_planted_at_planting) { create(:planting, owner: friend2, planted_at: nil) }
let!(:friend_photo) { create(:photo, owner: friend1) }
let!(:friend_post) { create(:post, author: friend2) }
let!(:liked_post) { create(:like, likeable: friend_photo, member: friend2) }
let!(:liked_photo) { create(:like, likeable: friend_post, member: friend1) }
before do
login_as(member)
@@ -37,8 +37,8 @@ describe "timeline", :js do
end
describe 'shows the friends you follow' do
it { expect(page).to have_link href: member_path(planting_friend) }
it { expect(page).to have_link href: member_path(post_friend) }
it { expect(page).to have_link href: member_path(friend1) }
it { expect(page).to have_link href: member_path(friend2) }
end
end
end

View File

@@ -77,90 +77,6 @@ describe Ability do
end
end
context 'plantings' do
let(:approved_crop) { create(:crop, approval_status: 'approved') }
let(:unapproved_crop) { create(:crop, approval_status: 'unapproved') }
let(:garden) { create(:garden, owner: member) }
let(:planting) { create(:planting, garden: garden, crop: approved_crop, owner: member) }
let(:other_planting) { create(:planting, crop: approved_crop) }
let(:planting_with_unapproved_crop) { create(:planting, garden: garden, crop: unapproved_crop, owner: member) }
it 'can create a planting' do
ability.should be_able_to(:create, Planting)
end
it 'can manage their own planting with an approved crop' do
ability.should be_able_to(:update, planting)
ability.should be_able_to(:destroy, planting)
end
xit "can't manage their own planting with an unapproved crop" do
ability.should_not be_able_to(:update, planting_with_unapproved_crop)
ability.should_not be_able_to(:destroy, planting_with_unapproved_crop)
end
it "can't manage another member's planting" do
ability.should_not be_able_to(:update, other_planting)
ability.should_not be_able_to(:destroy, other_planting)
end
it 'can transplant their own planting' do
ability.should be_able_to(:transplant, planting)
end
context 'garden collaborator' do
let(:garden) { create(:garden) }
let(:planting_in_garden) { create(:planting, garden:, crop: approved_crop, owner: garden.owner) }
before do
garden.garden_collaborators.create(member:)
end
it 'can manage plantings in a garden they collaborate on' do
ability.should be_able_to(:update, planting_in_garden)
ability.should be_able_to(:destroy, planting_in_garden)
end
it 'can transplant a planting in a garden they collaborate on' do
ability.should be_able_to(:transplant, planting_in_garden)
end
end
end
context 'harvests' do
let(:harvest) { create(:harvest, owner: member) }
let(:other_harvest) { create(:harvest) }
it 'can create a harvest' do
ability.should be_able_to(:create, Harvest)
end
it 'can manage their own harvest' do
ability.should be_able_to(:update, harvest)
ability.should be_able_to(:destroy, harvest)
end
it "can't manage another member's harvest" do
ability.should_not be_able_to(:update, other_harvest)
ability.should_not be_able_to(:destroy, other_harvest)
end
context 'garden collaborator' do
let(:garden) { create(:garden) }
let(:planting_in_garden) { create(:planting, garden:, owner: garden.owner) }
let(:harvest_in_garden) { create(:harvest, planting: planting_in_garden, owner: planting_in_garden.owner) }
before do
garden.garden_collaborators.create(member:)
end
it 'can manage harvests in a garden they collaborate on' do
ability.should be_able_to(:update, harvest_in_garden)
ability.should be_able_to(:destroy, harvest_in_garden)
end
end
end
context 'plant parts' do
let(:plant_part) { create(:plant_part) }
@@ -220,117 +136,9 @@ describe Ability do
it "can manage members" do
ability.should be_able_to(:destroy, create(:member))
end
it "cannot delete themselves" do
ability.should_not be_able_to(:destroy, member)
end
end
end
context 'activities' do
let(:activity) { create(:activity, owner: member) }
let(:other_activity) { create(:activity) }
it 'can create an activity' do
ability.should be_able_to(:create, Activity)
end
it 'can manage their own activity' do
ability.should be_able_to(:update, activity)
ability.should be_able_to(:destroy, activity)
end
it "can't manage another member's activity" do
ability.should_not be_able_to(:update, other_activity)
ability.should_not be_able_to(:destroy, other_activity)
end
context 'garden collaborator' do
let(:garden) { create(:garden) }
let(:activity_in_garden) { create(:activity, garden:) }
before do
garden.garden_collaborators.create(member:)
end
it 'can manage activities in a garden they collaborate on' do
ability.should be_able_to(:update, activity_in_garden)
ability.should be_able_to(:destroy, activity_in_garden)
end
end
end
context 'seeds' do
let(:seed) { create(:seed, owner: member) }
let(:other_seed) { create(:seed) }
it 'can create a seed' do
ability.should be_able_to(:create, Seed)
end
it 'can manage their own seed' do
ability.should be_able_to(:update, seed)
ability.should be_able_to(:destroy, seed)
end
it "can't manage another member's seed" do
ability.should_not be_able_to(:update, other_seed)
ability.should_not be_able_to(:destroy, other_seed)
end
end
context 'comments' do
let(:comment) { create(:comment, author: member) }
let(:other_comment) { create(:comment) }
it 'can create a comment' do
ability.should be_able_to(:create, Comment)
end
it 'can manage their own comment' do
ability.should be_able_to(:update, comment)
ability.should be_able_to(:destroy, comment)
end
it "can't manage another member's comment" do
ability.should_not be_able_to(:update, other_comment)
ability.should_not be_able_to(:destroy, other_comment)
end
end
context 'photos' do
let(:photo) { create(:photo, owner: member) }
let(:other_photo) { create(:photo) }
it 'can create a photo' do
ability.should be_able_to(:create, Photo)
end
it 'can manage their own photo' do
ability.should be_able_to(:update, photo)
ability.should be_able_to(:destroy, photo)
end
it "can't manage another member's photo" do
ability.should_not be_able_to(:update, other_photo)
ability.should_not be_able_to(:destroy, other_photo)
end
end
context 'likes' do
let(:like) { create(:like, member:) }
let(:other_like) { create(:like) }
it 'can create a like' do
ability.should be_able_to(:create, Like)
end
it 'can destroy their own like' do
ability.should be_able_to(:destroy, like)
end
it "can't destroy another member's like" do
ability.should_not be_able_to(:destroy, other_like)
end
end
end

View File

@@ -41,21 +41,6 @@ describe Comment do
end
end
context "when the post author has blocked the comment author" do
let(:post_author) { create(:member) }
let(:comment_author) { create(:member) }
let(:post) { create(:post, author: post_author) }
before do
post_author.blocks.create(blocked: comment_author)
end
it "is not valid" do
comment = build(:comment, commentable: post, author: comment_author)
expect(comment).not_to be_valid
end
end
context "ordering" do
before do
@m = create(:member)

View File

@@ -297,54 +297,54 @@ describe Crop do
subject { described_class.interesting }
# first, a couple of candidate crops
let(:first_crop) { create(:crop) }
let(:second_crop) { create(:crop) }
let(:crop1) { create(:crop) }
let(:crop2) { create(:crop) }
let(:first_crop_planting) { first_crop.plantings.first }
let(:second_crop_planting) { second_crop.plantings.first }
let(:crop1_planting) { crop1.plantings.first }
let(:crop2_planting) { crop2.plantings.first }
let(:member) { create(:member, login_name: 'pikachu') }
describe 'lists interesting crops' do
before do
# they need 3+ plantings each to be interesting
create_list(:planting, 3, crop: first_crop, owner: member)
create_list(:planting, 3, crop: second_crop, owner: member)
create_list(:planting, 3, crop: crop1, owner: member)
create_list(:planting, 3, crop: crop2, owner: member)
# crops need 3+ photos to be interesting
first_crop_planting.photos = create_list :photo, 3, owner: member
second_crop_planting.photos = create_list :photo, 3, owner: member
crop1_planting.photos = create_list :photo, 3, owner: member
crop2_planting.photos = create_list :photo, 3, owner: member
end
it { is_expected.to include first_crop }
it { is_expected.to include second_crop }
it { is_expected.to include crop1 }
it { is_expected.to include crop2 }
it { expect(subject.size).to eq 2 }
end
describe 'crops without plantings are not interesting' do
before do
# only first_crop has plantings
create_list(:planting, 3, crop: first_crop, owner: member)
# only crop1 has plantings
create_list(:planting, 3, crop: crop1, owner: member)
# ... and photos
first_crop_planting.photos = create_list(:photo, 3, owner: member)
crop1_planting.photos = create_list(:photo, 3, owner: member)
end
it { is_expected.to include first_crop }
it { is_expected.not_to include second_crop }
it { is_expected.to include crop1 }
it { is_expected.not_to include crop2 }
it { expect(subject.size).to eq 1 }
end
describe 'crops without photos are not interesting' do
before do
# both crops have plantings
create_list(:planting, 3, crop: first_crop, owner: member)
create_list(:planting, 3, crop: second_crop, owner: member)
create_list(:planting, 3, crop: crop1, owner: member)
create_list(:planting, 3, crop: crop2, owner: member)
# but only first_crop has photos
first_crop_planting.photos = create_list(:photo, 3, owner: member)
# but only crop1 has photos
crop1_planting.photos = create_list(:photo, 3, owner: member)
end
it { is_expected.to include first_crop }
it { is_expected.not_to include second_crop }
it { is_expected.to include crop1 }
it { is_expected.not_to include crop2 }
it { expect(subject.size).to eq 1 }
end
end

View File

@@ -63,21 +63,6 @@ describe Like do
expect(Like.all).not_to include like
end
context "when the likeable author has blocked the member" do
let(:likeable_author) { create(:member) }
let(:post_author) { create(:member) }
let(:post) { create(:post, author: likeable_author) }
before do
likeable_author.blocks.create(blocked: member)
end
it "is not valid" do
like = build(:like, likeable: post, member: member)
expect(like).not_to be_valid
end
end
it 'liked_by_members_names' do
expect(post.liked_by_members_names).to eq []
Like.create(member:, likeable: post)

View File

@@ -102,10 +102,10 @@ describe Member do
context 'newsletter scope' do
it 'finds newsletter recipients' do
regular_member = create(:member)
newsletter_member = create(:newsletter_recipient_member)
Member.wants_newsletter.should include newsletter_member
Member.wants_newsletter.should_not include regular_member
member1 = create(:member)
member2 = create(:newsletter_recipient_member)
Member.wants_newsletter.should include member2
Member.wants_newsletter.should_not include member1
end
end
@@ -299,31 +299,31 @@ describe Member do
end
context 'member who followed another member' do
let(:follower) { create(:member) }
let(:followed_member) { create(:member) }
let(:other_member) { create(:member) }
let(:member1) { create(:member) }
let(:member2) { create(:member) }
let(:member3) { create(:member) }
before do
@follow = follower.follows.create(follower_id: follower.id, followed_id: followed_member.id)
@follow = member1.follows.create(follower_id: member1.id, followed_id: member2.id)
end
context 'already_following' do
it 'detects that member is already following a member' do
expect(follower.already_following?(followed_member)).to be true
expect(member1.already_following?(member2)).to be true
end
it 'detects that member is not already following a member' do
expect(follower.already_following?(other_member)).to be false
expect(member1.already_following?(member3)).to be false
end
end
context 'get_follow' do
it 'gets the correct follow for a followed member' do
expect(follower.get_follow(followed_member).id).to eq @follow.id
expect(member1.get_follow(member2).id).to eq @follow.id
end
it 'returns nil for a member that is not followed' do
expect(follower.get_follow(other_member)).to be_nil
expect(member1.get_follow(member3)).to be_nil
end
end
end

View File

@@ -3,53 +3,11 @@
require 'rails_helper'
describe "Forums" do
let(:admin) { create(:admin_member) }
let(:forum) { create(:forum) }
describe "GET /forums" do
it "returns a successful response" do
it "works! (now write some real specs)" do
# Run the generator again with the --webrat flag if you want to use webrat methods/matchers
get forums_path
expect(response).to have_http_status(:ok)
end
it "returns JSON when requested" do
get forums_path(format: :json)
expect(response).to have_http_status(:ok)
expect(response.content_type).to include("application/json")
end
end
describe "GET /forums/:id" do
it "returns a successful response" do
get forum_path(forum)
expect(response).to have_http_status(:ok)
end
it "returns JSON when requested" do
get forum_path(forum, format: :json)
expect(response).to have_http_status(:ok)
expect(response.content_type).to include("application/json")
end
end
describe "POST /forums" do
context "as an admin" do
before { sign_in admin }
it "creates a new forum" do
expect {
post forums_path, params: { forum: { name: "New Request Forum", description: "Desc", owner_id: admin.id } }
}.to change(Forum, :count).by(1)
expect(response).to redirect_to(forum_path(Forum.last))
end
end
context "as a guest" do
it "redirects to sign in or denies access" do
post forums_path, params: { forum: { name: "New Request Forum", description: "Desc" } }
# Depending on CanCan/Devise setup, it might be a redirect to login or root
expect(response).to redirect_to(new_member_session_path).or redirect_to(root_path)
end
response.status.should be(200)
end
end
end

View File

@@ -4,17 +4,17 @@ require 'rails_helper'
describe "forums/index" do
let(:admin) { create(:admin_member) }
let(:first_forum) { create(:forum) }
let(:second_forum) { create(:forum) }
let(:forum1) { create(:forum) }
let(:forum2) { create(:forum) }
before do
controller.stub(:current_user) { admin }
assign(:forums, [first_forum, second_forum])
assign(:forums, [forum1, forum2])
end
it "renders a list of forums" do
render
assert_select "h2", text: first_forum.name, count: 2
assert_select "h2", text: forum1.name, count: 2
end
it "doesn't display posts for empty forums" do
@@ -23,7 +23,7 @@ describe "forums/index" do
end
context "posts" do
let!(:post) { create(:forum_post, forum: first_forum) }
let!(:post) { create(:forum_post, forum: forum1) }
let!(:comment) { create(:comment, commentable: post) }
before { render }

View File

@@ -33,4 +33,12 @@ describe 'home/index.html.haml', type: "view" do
rendered.should have_content @member.location
end
end
context 'signed in' do
before do
sign_in @member
controller.stub(:current_user) { @member }
render
end
end
end

View File

@@ -0,0 +1,13 @@
# frozen_string_literal: true
require 'rails_helper'
describe "photos/edit" do
before do
@photo = assign(:photo, stub_model(Photo,
owner_id: 1,
flickr_photo_id: 1,
thumbnail_url: "MyString",
fullsize_url: "MyString"))
end
end

View File

@@ -43,6 +43,16 @@ describe "posts/_single" do
end
end
context "when logged in as post author" do
before do
@member = create(:member)
sign_in @member
controller.stub(:current_user) { @member }
@post = create(:post, author: @member)
render_post
end
end
context "when post has been edited" do
before do
@member = create(:member)