Merge branch 'dev' into datepicker

This commit is contained in:
Daniel O'Connor
2024-10-13 13:16:22 +10:30
committed by GitHub
22 changed files with 343 additions and 80 deletions

View File

@@ -106,15 +106,37 @@ jobs:
- name: Prepare database for testing
run: bundle exec rails db:prepare
- name: Run rspec (lib)
run: bundle exec rspec spec/lib/ -fd --fail-fast
- name: Run rspec (services)
run: bundle exec rspec spec/services/ -fd --fail-fast
- name: Run rspec (models)
run: bundle exec rspec spec/models/ -fd --fail-fast
- name: Run rspec (controllers)
run: bundle exec rspec spec/controllers/ -fd --fail-fast
- name: Run rspec (views)
run: bundle exec rspec spec/views/ -fd --fail-fast
- name: Run rspec (routing)
run: bundle exec rspec spec/routing/ -fd --fail-fast
- name: Run rspec (request)
run: bundle exec rspec spec/requests/ -fd --fail-fast
- name: precompile assets
run: bundle exec rails assets:precompile
- name: index into elastic search
run: bundle exec rails search:reindex
- name: Run rspec (report results to Percy.io and CodeClimate)
run: bundle exec rspec spec -fd
run: bundle exec rspec spec/features/ -fd --fail-fast
- name: Report to code climate
run: |

View File

@@ -158,7 +158,7 @@ Metrics/BlockLength:
# Offense count: 7
# Configuration parameters: CountComments, CountAsOne.
Metrics/ClassLength:
Max: 183
Max: 188
# Offense count: 6
# Configuration parameters: AllowedMethods, AllowedPatterns.

View File

@@ -80,8 +80,8 @@ GEM
active_link_to (1.0.5)
actionpack
addressable
active_median (0.4.1)
activesupport (>= 6.1)
active_median (0.5.0)
activesupport (>= 7)
active_record_union (1.3.0)
activerecord (>= 4.0)
active_utils (3.4.1)
@@ -290,7 +290,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
haml_lint (0.58.0)
haml_lint (0.59.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -329,7 +329,7 @@ GEM
mini_magick (>= 4.9.5, < 5)
ruby-vips (>= 2.0.17, < 3)
io-console (0.7.2)
irb (1.14.0)
irb (1.14.1)
rdoc (>= 4.0.0)
reline (>= 0.4.2)
jquery-rails (4.6.0)
@@ -337,8 +337,8 @@ GEM
railties (>= 4.2.0)
thor (>= 0.14, < 2.0)
json (2.7.2)
json-schema (4.3.1)
addressable (>= 2.8)
json-schema (5.0.1)
addressable (~> 2.8)
jsonapi-resources (0.10.7)
activerecord (>= 4.1)
concurrent-ruby
@@ -388,7 +388,7 @@ GEM
mini_portile2 (2.8.7)
minitest (5.25.1)
moneta (1.0.0)
msgpack (1.7.2)
msgpack (1.7.3)
multi_json (1.15.0)
multi_xml (0.7.1)
bigdecimal (~> 3.1)
@@ -527,7 +527,7 @@ GEM
rspec-core (~> 3.13.0)
rspec-expectations (~> 3.13.0)
rspec-mocks (~> 3.13.0)
rspec-activemodel-mocks (1.2.0)
rspec-activemodel-mocks (1.2.1)
activemodel (>= 3.0)
activesupport (>= 3.0)
rspec-mocks (>= 2.99, < 4.0)
@@ -536,7 +536,7 @@ GEM
rspec-expectations (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-mocks (3.13.1)
rspec-mocks (3.13.2)
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-rails (7.0.1)
@@ -551,15 +551,15 @@ GEM
rspectre (0.1.0)
parser (>= 3.2.2.1)
rspec (~> 3.9)
rswag-api (2.14.0)
rswag-api (2.15.0)
activesupport (>= 5.2, < 8.0)
railties (>= 5.2, < 8.0)
rswag-specs (2.14.0)
rswag-specs (2.15.0)
activesupport (>= 5.2, < 8.0)
json-schema (>= 2.2, < 5.0)
json-schema (>= 2.2, < 6.0)
railties (>= 5.2, < 8.0)
rspec-core (>= 2.14)
rswag-ui (2.14.0)
rswag-ui (2.15.0)
actionpack (>= 5.2, < 8.0)
railties (>= 5.2, < 8.0)
rubocop (1.66.1)
@@ -585,7 +585,7 @@ GEM
rubocop-ast (>= 1.31.1, < 2.0)
rubocop-rake (0.6.0)
rubocop (~> 1.0)
rubocop-rspec (3.0.5)
rubocop-rspec (3.1.0)
rubocop (~> 1.61)
rubocop-rspec_rails (2.30.0)
rubocop (~> 1.61)
@@ -645,7 +645,7 @@ GEM
temple (0.10.3)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
terser (1.2.3)
terser (1.2.4)
execjs (>= 0.3.0, < 3)
thor (1.3.2)
thread_safe (0.3.6)

View File

@@ -0,0 +1,87 @@
# frozen_string_literal: true
class GardenCollaboratorsController < ApplicationController
before_action :authenticate_member!, except: %i(index show)
before_action :load_garden
load_and_authorize_resource id_param: :slug
respond_to :html
responders :flash
def index
@garden_collaborators = @garden.garden_collaborators.paginate(page: params[:page])
respond_with(@garden_collaborators)
end
def show
@garden_collaborator = GardenCollaborator.find(params[:garden_collaborator_id])
respond_with(@garden_collaborator)
end
def new
@garden_collaborator = GardenCollaborator.new(garden: @garden)
authorize! :create, @garden_collaborator
respond_with(@garden_collaborator)
end
def edit
@garden_collaborator = GardenCollaborator.find(params[:id])
authorize! :update, @garden_collaborator
respond_with(@garden_collaborator)
end
def create
@garden_collaborator = GardenCollaborator.new(garden: @garden)
authorize! :create, @garden_collaborator
@member = Member.find_by(slug: params[:garden_collaborator][:member_slug])
@garden_collaborator.member = @member
if @garden_collaborator.save
redirect_to garden_garden_collaborators_path(@garden)
else
respond_with(@garden_collaborator)
end
end
def update
@garden_collaborator = GardenCollaborator.find(params[:id])
authorize! :update, @garden_collaborator
@member = Member.find_by(slug: params[:garden_collaborator][:member_slug])
@garden_collaborator.member = @member
@garden_collaborator.save
respond_with(@garden_collaborator)
end
def destroy
@garden_collaborator = GardenCollaborator.find(params[:id])
authorize! :destroy, @garden_collaborator
if @garden_collaborator.destroy
redirect_to garden_garden_collaborators_path(@garden)
else
respond_with(@garden_collaborator)
end
end
private
def load_garden
@garden = Garden.find_by(slug: params[:garden_slug])
end
def garden_collaborator_params
params.require(:garden_collaborator).permit(
:member_slug
)
end
end

View File

@@ -8,7 +8,10 @@ class GardensController < DataController
@gardens = @gardens.includes(:owner)
@gardens = @gardens.active unless @show_all
@gardens = @gardens.where(owner: @owner) if @owner.present?
if @owner.present?
@gardens = @gardens.left_joins(:garden_collaborators)
@gardens = @gardens.where(owner: @owner).or(@gardens.where(garden_collaborators: { member: @owner }))
end
@gardens = @gardens.where.not(members: { confirmed_at: nil })
.order(:name).paginate(page: params[:page])
respond_with(@gardens)

View File

@@ -108,16 +108,38 @@ class Ability
can :create, Planting
can :update, Planting, garden: { owner_id: member.id }, crop: { approval_status: 'approved' }
can :destroy, Planting, garden: { owner_id: member.id }, crop: { approval_status: 'approved' }
can :update, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :destroy, Planting do |planting|
planting.garden.garden_collaborators.where(member_id: member.id).any?
end
can :create, GardenCollaborator, garden: { owner_id: member.id }
can :update, GardenCollaborator, garden: { owner_id: member.id }
can :destroy, GardenCollaborator, garden: { owner_id: member.id }
can :create, Activity
can :update, Activity, owner_id: member.id
can :destroy, Activity, owner_id: member.id
can :update, Activity do |activity|
activity.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :destroy, Activity do |activity|
activity.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :create, Harvest
can :update, Harvest, owner_id: member.id
can :destroy, Harvest, owner_id: member.id
can :update, Harvest, owner_id: member.id, planting: { owner_id: member.id }
can :destroy, Harvest, owner_id: member.id, planting: { owner_id: member.id }
can :update, Harvest do |harvest|
harvest.planting&.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :destroy, Harvest do |harvest|
harvest.planting&.garden&.garden_collaborators&.where(member_id: member.id)&.any?
end
can :create, Photo
can :update, Photo, owner_id: member.id

View File

@@ -10,6 +10,7 @@ class Garden < ApplicationRecord
has_many :plantings, dependent: :destroy
has_many :crops, through: :plantings
has_many :activities, dependent: :destroy
has_many :garden_collaborators, dependent: :destroy
belongs_to :garden_type, optional: true

View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
class GardenCollaborator < ApplicationRecord
belongs_to :member
belongs_to :garden
validates :member_id, uniqueness: { scope: :garden }
validate :not_garden_owner
def not_garden_owner
return unless member
return unless garden
errors.add(:member_id, "cannot be the garden owner") if garden.owner == member
end
def member_slug
@member&.slug
end
def member_slug=(_slug)
member_slug
end
end

View File

@@ -153,7 +153,10 @@ class Harvest < ApplicationRecord
def owner_must_match_planting
return if planting.blank? # only check if we are linked to a planting
errors.add(:owner, "of harvest must be the same as planting") unless owner == planting.owner
return if owner == planting.owner || planting.garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner,
"of harvest must be the same as planting, or a collaborator on that garden")
end
def harvest_must_be_after_planting

View File

@@ -127,6 +127,9 @@ class Planting < ApplicationRecord
end
def owner_must_match_garden_owner
errors.add(:owner, "must be the same as garden") unless owner == garden.owner
return if owner == garden.owner || garden.garden_collaborators.where(member_id: owner).any?
errors.add(:owner,
"must be the same as garden, or a collaborator on that garden")
end
end

View File

@@ -0,0 +1,19 @@
.card.col-md-8.col-lg-7.mx-auto.float-none.white.z-depth-1.py-2.px-2
= bootstrap_form_for(@garden_collaborator.new_record? ? [@garden, @garden_collaborator] : garden_garden_collaborator_path(@garden, @garden_collaborator)) do |f|
.card-body
= required_field_help_text
- if @garden_collaborator.errors.any?
#error_explanation.alert.alert-warning{:role => "alert"}
%h4.alert-heading
= pluralize(@garden_collaborator.errors.size, "error")
prohibited this garden collaborator from being saved
%ul
- @garden_collaborator.errors.full_messages.each do |msg|
%li= msg
.alert.alert-info
Ask your friend, family member or community garden member for their growstuff username to add them as a collaborator on your garden.
= f.text_field :member_slug, maxlength: 255, required: true
.row
.card-footer
.text-right= f.submit 'Save Collaboator'

View File

@@ -0,0 +1,3 @@
- content_for :title, "Edit garden collaborator"
= render 'form'

View File

@@ -0,0 +1,72 @@
- content_for :title, "#{@garden} collaborators"
%h1= "#{@garden} collaborators"
- content_for :breadcrumbs do
%li.breadcrumb-item= link_to 'Gardens', gardens_path
%li.breadcrumb-item.active= link_to @garden, gardens_path(@garden)
.row
.col-md-2
- if current_member.present?
.flex-column.nav-pills.layout-nav{"role" => "tablist", "aria-orientation"=>"vertical"}
- if can?(:create, GardenCollaborator.new(garden: @garden))
= link_to url_for([@garden, GardenCollaborator.new(garden: @garden), action: :new]), class: 'btn' do
Add a #{GardenCollaborator.new(garden: @garden).model_name.human}
- else
= render 'shared/signin_signup', to: "record your #{model.to_s.pluralize.downcase}"
%hr/
%p.text-center
#{ENV['GROWSTUFF_SITE_NAME']} helps you track what you're
harvesting from your home garden and see how productive it is.
.col-md-10
- if @garden_collaborators.empty?
%p There are no collaborators to display.
- if can?(:create, GardenCollaborator) && @owner == current_member
= link_to 'Add a garden collaborator', new_garden_garden_collaborator_path, class: 'btn btn-primary'
- else
%section
%h2= page_entries_info @garden_collaborators
= will_paginate @garden_collaborators
- @garden_collaborators.each do |garden_collaborator|
- member = garden_collaborator.member
- cache member do
.card
.card-body
- if can?(:destroy, garden_collaborator)
%div{"style": "float: right"}
= link_to garden_garden_collaborator_path(@garden, garden_collaborator), method: :delete, class: "btn btn-danger" do
Remove access
%h4.login-name= link_to member, member
%div
= render "members/avatar", member: member
%div
= link_to "view all #{member}'s gardens", member_gardens_path(member)
%p
%small
Joined
= distance_of_time_in_words(member.created_at, Time.zone.now)
ago.
- if member.location.present?
= link_to member.location, place_path(member.location)
.card-footer
%ul.nav.nav-justified.small
%li.nav-item.border-right
= link_to member_plantings_path(member) do
= localize_plural(member.plantings.active, Planting)
%li.nav-item.border-right
= link_to member_harvests_path(member) do
= localize_plural(member.harvests, Harvest)
%li.nav-item
= link_to member_seeds_path(member) do
= localize_plural(member.seeds.active, Seed)
.row
.col-12= page_entries_info @garden_collaborators
.col-12= will_paginate @garden_collaborators

View File

@@ -0,0 +1,3 @@
- content_for :title, "New garden collaborator"
= render 'form'

View File

View File

@@ -15,7 +15,7 @@
%li= msg
= f.text_field :name, maxlength: 255, required: true
= f.text_area :description, rows: 6, placeholder: "Share more about this garden - when did you first start it? How is it watered? Are you focused on any particular techniques? What are your plans and goals?"
= f.text_area :description, rows: 6, placeholder: "Tell us about this garden - where is it located? What does it look like? Do you have a link to a photo? Do you have irrigation? What are your plans?"
= f.text_field :location,
value: @garden.location || current_member.location,
class: 'form-control', maxlength: 255

View File

@@ -31,7 +31,7 @@
%p
:markdown
#{strip_tags markdownify(@garden.description)}
- unless @garden.description
- unless @garden.description.present?
.row-fluid
%p No description available yet.
@@ -39,8 +39,13 @@
%p
Why not
= link_to 'tell us more.', edit_garden_path(@garden)
- else
- if can? :edit, @garden
%p
Did you want to
= link_to 'update this description.', edit_garden_path(@garden)
- if @garden.plantings.where.not(planted_at: nil).any?
- if @garden.plantings.active.any?
%section.card
%h2 Garden progress
.card-body
@@ -55,6 +60,14 @@
- else
.col-md-12
%p Nothing is currently planted here.
- if can?(:edit, @garden)
.col-md-12
= garden_plant_something_button(@garden)
- if can?(:destroy, @garden)
.dropdown-divider
= delete_button(@garden, classes: 'dropdown-item text-danger',
message: 'All plantings associated with this garden will also be deleted. Are you sure?')
%section
%h2 Current activities in garden
@@ -65,6 +78,9 @@
- else
.col-md-12
%p Nothing is currently planned here.
- if can?(:edit, @garden)
.col-md-12
= garden_plan_something_button(@garden)
- if @suggested_companions.any?
%section.companions
@@ -83,6 +99,17 @@
%p
%strong Owner:
= link_to @garden.owner, @garden.owner
%p
%strong Collaborators:
- if can?(:create, GardenCollaborator.new(garden: @garden))
= link_to "Manage", garden_garden_collaborators_path(@garden)
- if @garden.garden_collaborators.any?
%ul
- @garden.garden_collaborators.each do |collabator|
%li= link_to collabator.member, collabator.member
- else
None
- if @garden.location.present?
%p
%strong Location:

View File

@@ -30,6 +30,8 @@ Rails.application.routes.draw do
resources :gardens, concerns: :has_photos, param: :slug do
get 'timeline' => 'charts/gardens#timeline', constraints: { format: 'json' }
resources :garden_collaborators
end
resources :plantings, concerns: :has_photos, param: :slug do

View File

@@ -0,0 +1,10 @@
class CreateGardenCollaborators < ActiveRecord::Migration[7.2]
def change
create_table :garden_collaborators do |t|
t.references :member
t.references :garden
t.timestamps
t.index [:member_id, :garden_id], unique: true
end
end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
ActiveRecord::Schema[7.2].define(version: 2024_09_29_041435) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -54,13 +54,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.string "slug"
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.integer "cached_weighted_score", default: 0
t.integer "cached_weighted_total", default: 0
t.float "cached_weighted_average", default: 0.0
t.integer "likes_count", default: 0
t.index ["garden_id"], name: "index_activities_on_garden_id"
t.index ["owner_id"], name: "index_activities_on_owner_id"
@@ -214,13 +207,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.text "body", null: false
t.datetime "created_at", precision: nil
t.datetime "updated_at", precision: nil
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.integer "cached_weighted_score", default: 0
t.integer "cached_weighted_total", default: 0
t.float "cached_weighted_average", default: 0.0
end
create_table "crop_companions", force: :cascade do |t|
@@ -280,6 +266,16 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.index ["slug"], name: "index_forums_on_slug", unique: true
end
create_table "garden_collaborators", force: :cascade do |t|
t.bigint "member_id"
t.bigint "garden_id"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["garden_id"], name: "index_garden_collaborators_on_garden_id"
t.index ["member_id", "garden_id"], name: "index_garden_collaborators_on_member_id_and_garden_id", unique: true
t.index ["member_id"], name: "index_garden_collaborators_on_member_id"
end
create_table "garden_types", force: :cascade do |t|
t.text "name", null: false
t.text "slug", null: false
@@ -504,13 +500,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.datetime "date_taken", precision: nil
t.integer "likes_count", default: 0
t.string "source"
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.integer "cached_weighted_score", default: 0
t.integer "cached_weighted_total", default: 0
t.float "cached_weighted_average", default: 0.0
t.index ["fullsize_url"], name: "index_photos_on_fullsize_url", unique: true
t.index ["thumbnail_url"], name: "index_photos_on_thumbnail_url", unique: true
end
@@ -553,13 +542,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.integer "days_to_last_harvest"
t.integer "parent_seed_id"
t.integer "harvests_count", default: 0
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.integer "cached_weighted_score", default: 0
t.integer "cached_weighted_total", default: 0
t.float "cached_weighted_average", default: 0.0
t.integer "likes_count", default: 0
t.index ["slug"], name: "index_plantings_on_slug", unique: true
end
@@ -574,13 +556,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.integer "forum_id"
t.integer "likes_count", default: 0
t.integer "comments_count", default: 0
t.integer "cached_votes_total", default: 0
t.integer "cached_votes_score", default: 0
t.integer "cached_votes_up", default: 0
t.integer "cached_votes_down", default: 0
t.integer "cached_weighted_score", default: 0
t.integer "cached_weighted_total", default: 0
t.float "cached_weighted_average", default: 0.0
t.index ["created_at", "author_id"], name: "index_posts_on_created_at_and_author_id"
t.index ["slug"], name: "index_posts_on_slug", unique: true
end
@@ -628,22 +603,6 @@ ActiveRecord::Schema[7.1].define(version: 2024_07_14_024918) do
t.index ["slug"], name: "index_seeds_on_slug", unique: true
end
create_table "votes", force: :cascade do |t|
t.string "votable_type"
t.bigint "votable_id"
t.string "voter_type"
t.bigint "voter_id"
t.boolean "vote_flag"
t.string "vote_scope"
t.integer "vote_weight"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
t.index ["votable_id", "votable_type", "vote_scope"], name: "index_votes_on_votable_id_and_votable_type_and_vote_scope"
t.index ["votable_type", "votable_id"], name: "index_votes_on_votable"
t.index ["voter_id", "voter_type", "vote_scope"], name: "index_votes_on_voter_id_and_voter_type_and_vote_scope"
t.index ["voter_type", "voter_id"], name: "index_votes_on_voter"
end
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 "harvests", "plantings"

View File

@@ -0,0 +1,6 @@
FactoryBot.define do
factory :garden_collaborator do
garden
member
end
end

View File

@@ -7,9 +7,6 @@ require 'simplecov'
# output coverage locally AND send it to coveralls
SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter.new([SimpleCov::Formatter::HTMLFormatter])
# fail if there's a significant test coverage drop
SimpleCov.maximum_coverage_drop 1
require 'spec_helper'
require File.expand_path('../config/environment', __dir__)
require 'rspec/rails'