Merge pull request #2320 from Br3nda/cache/harvests

Reading #index lookup from ElasticSearch instead of the database
This commit is contained in:
Brenda Wallace
2020-01-13 19:59:25 +13:00
committed by GitHub
102 changed files with 1141 additions and 551 deletions

View File

@@ -14,7 +14,7 @@ $(document).ready(function() {
} else {
likeBadge.removeClass('liked');
likeButton.data('method', 'post');
likeButton.attr('href', '/likes.json?post_id=' + data.id);
likeButton.attr('href', '/likes.json?type=Post&id=' + data.id);
likeButton.text('Like');
}
});
@@ -34,7 +34,7 @@ $(document).ready(function() {
likeBadge.removeClass('liked');
// Turn the button into an *like* button
likeButton.data('method', 'post');
likeButton.attr('href', '/likes.json?photo_id=' + data.id);
likeButton.attr('href', '/likes.json?type=Photo&id=' + data.id);
}
});
});

View File

@@ -11,7 +11,10 @@ class CropsController < ApplicationController
def index
@sort = params[:sort]
@crops = crops
@crops = Crop.search('*', boost_by: %i(plantings_count harvests_count),
limit: 100,
page: params[:page],
load: false)
@num_requested_crops = requested_crops.size if current_member
@filename = filename
respond_with @crops
@@ -51,19 +54,19 @@ class CropsController < ApplicationController
def search
@term = params[:term]
@crops = CropSearchService.search(
@term, page: params[:page],
per_page: 36,
current_member: current_member
)
@crops = CropSearchService.search(@term,
page: params[:page],
per_page: Crop.per_page,
current_member: current_member)
respond_with @crops
end
def show
@crop = Crop.includes(:scientific_names, :alternate_names, :parent, :varieties).find_by!(slug: params[:slug])
@crop = Crop.includes(
:scientific_names, :alternate_names, :parent, :varieties
).find_by!(slug: params[:slug])
respond_to do |format|
format.html do
@photos = Photo.by_crop(@crop)
@posts = @crop.posts.order(created_at: :desc).paginate(page: params[:page])
@companions = @crop.companions.approved
end
@@ -204,24 +207,17 @@ class CropsController < ApplicationController
def crop_json_fields
{
include: {
plantings: {
plantings: {
include: {
owner: { only: %i(id login_name location latitude longitude) }
}
},
scientific_names: { only: [:name] },
alternate_names: { only: [:name] }
alternate_names: { only: [:name] }
}
}
end
def crops
q = Crop.approved.includes(:scientific_names, plantings: :photos)
q = q.popular unless @sort == 'alpha'
q.order(Arel.sql("LOWER(crops.name)"))
.includes(:photos).paginate(page: params[:page])
end
def requested_crops
current_member.requested_crops.pending_approval
end

View File

@@ -52,7 +52,9 @@ class GardensController < DataController
private
def garden_params
params.require(:garden).permit(:name, :slug, :description, :active,
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id)
params.require(:garden).permit(
:name, :slug, :description, :active,
:location, :latitude, :longitude, :area, :area_unit, :garden_type_id
)
end
end

View File

@@ -4,14 +4,27 @@ class HarvestsController < DataController
after_action :update_crop_medians, only: %i(create update destroy)
def index
@owner = Member.find_by(slug: params[:member_slug])
@crop = Crop.find_by(slug: params[:crop_slug])
@planting = Planting.find_by(slug: params[:planting_id])
where = {}
if params[:member_slug]
@owner = Member.find_by(slug: params[:member_slug])
where['owner_id'] = @owner.id
end
@harvests = @harvests.where(owner: @owner) if @owner.present?
@harvests = @harvests.where(crop: @crop) if @crop.present?
@harvests = @harvests.where(planting: @planting) if @planting.present?
@harvests = @harvests.order(harvested_at: :desc).joins(:owner, :crop).paginate(page: params[:page])
if params[:crop_slug]
@crop = Crop.find_by(slug: params[:crop_slug])
where['crop_id'] = @crop.id
end
if params[:planting_slug]
@planting = Planting.find_by(slug: params[:planting_slug])
where['planting_id'] = @planting.id
end
@harvests = Harvest.search('*', where: where,
limit: 100,
page: params[:page],
load: false,
boost_by: [:created_at])
@filename = csv_filename
@@ -26,7 +39,7 @@ class HarvestsController < DataController
def new
@harvest = Harvest.new(harvested_at: Time.zone.today)
@planting = Planting.find_by(slug: params[:planting_id]) if params[:planting_id]
@planting = Planting.find_by(slug: params[:planting_slug]) if params[:planting_slug]
@crop = Crop.find_by(id: params[:crop_id])
respond_with(@harvest)
end

View File

@@ -5,36 +5,42 @@ class LikesController < ApplicationController
respond_to :html, :json
def create
@like = Like.new(member: current_member, likeable: find_likeable)
return failed(@like, message: 'Unable to like') unless @like.likeable && @like.save
success(@like, liked_by_member: true, status_code: :created)
@like = Like.new(
member: current_member,
likeable_type: params[:type],
likeable_id: params[:id]
)
if @like.likeable && @like.save
@like.likeable.reindex(refresh: true)
success(@like, liked_by_member: true, status_code: :created)
else
failed(@like, message: 'Unable to like')
end
end
def destroy
@like = Like.find_by(id: params[:id], member: current_member)
return failed(@like, message: 'Unable to unlike') unless @like&.destroy
@like = Like.find_by(
likeable_type: params[:type],
likeable_id: params[:id],
member: current_member
)
success(@like, liked_by_member: false, status_code: :ok)
if @like&.destroy
@like.likeable.reindex(refresh: true)
success(@like, liked_by_member: false, status_code: :ok)
else
failed(@like, message: 'Unable to unlike')
end
end
private
def find_likeable
if params[:post_id]
Post.find(params[:post_id])
elsif params[:photo_id]
Photo.find(params[:photo_id])
end
end
def render_json(like, liked_by_member: true)
{
id: like.likeable.id,
like_count: like.likeable.likes.count,
id: like.likeable.id,
like_count: like.likeable.likes.count,
liked_by_member: liked_by_member,
description: ActionController::Base.helpers.pluralize(like.likeable.likes.count, "like"),
url: like_path(like, format: :json)
description: ActionController::Base.helpers.pluralize(like.likeable.likes.count, "like")
}
end
@@ -42,9 +48,10 @@ class LikesController < ApplicationController
respond_to do |format|
format.html { redirect_to like.likeable }
format.json do
render(json: render_json(like,
liked_by_member: liked_by_member),
status: status_code)
render(json: render_json(
like,
liked_by_member: liked_by_member
), status: status_code)
end
end
end

View File

@@ -71,7 +71,7 @@ class MembersController < ApplicationController
EMAIL_TYPE_STRING = {
send_notification_email: "direct message notifications",
send_planting_reminder: "planting reminders"
send_planting_reminder: "planting reminders"
}.freeze
def member_params

View File

@@ -13,18 +13,22 @@ class PhotosController < ApplicationController
end
def index
where = {}
if params[:crop_slug]
@crop = Crop.find params[:crop_slug]
@photos = Photo.by_crop(@crop)
where = { crops: @crop.id }
elsif params[:planting_id]
@planting = Planting.find params[:planting_id]
@photos = @planting.photos
else
@photos = Photo.all
where = { planting_id: @planting.id }
end
@photos = @photos.order(created_at: :desc)
.includes(:owner)
.paginate(page: params[:page])
@photos = Photo.search(
load: false,
boost_by: [:created_at],
where: where,
page: params[:page],
limit: Photo.per_page
)
respond_with(@photos)
end
@@ -86,7 +90,7 @@ class PhotosController < ApplicationController
def find_or_create_photo_from_flickr_photo
photo = Photo.find_or_initialize_by(
source_id: photo_params[:source_id],
source: 'flickr'
source: 'flickr'
)
photo.update(photo_params)
photo.owner_id = current_member.id

View File

@@ -5,20 +5,28 @@ class PlantingsController < DataController
after_action :update_planting_medians, only: :update
def index
@owner = Member.find_by(slug: params[:member_slug]) if params[:member_slug]
@crop = Crop.find_by(slug: params[:crop_slug]) if params[:crop_slug]
@show_all = params[:all] == '1'
@plantings = @plantings.where(owner: @owner) if @owner.present?
@plantings = @plantings.where(crop: @crop) if @crop.present?
where = {}
where['active'] = true unless @show_all
@plantings = @plantings.active unless params[:all] == '1'
if params[:member_slug]
@owner = Member.find_by(slug: params[:member_slug])
where['owner_id'] = @owner.id
end
@plantings = @plantings.joins(:owner, :crop, :garden)
.order(created_at: :desc)
.includes(:owner, :garden, crop: :parent)
.paginate(page: params[:page])
if params[:crop_slug]
@crop = Crop.find_by(slug: params[:crop_slug])
where['crop_id'] = @crop.id
end
@plantings = Planting.search(
where: where,
page: params[:page],
limit: 30,
boost_by: [:created_at],
load: false
)
@filename = "Growstuff-#{specifics}Plantings-#{Time.zone.now.to_s(:number)}.csv"
@@ -27,9 +35,13 @@ class PlantingsController < DataController
def show
@photos = @planting.photos.includes(:owner).order(date_taken: :desc)
@harvests = Harvest.search(where: { planting_id: @planting.id })
@matching_seeds = matching_seeds
# TODO: use elastic search long/lat
@neighbours = @planting.nearby_same_crop
.where.not(id: @planting.id)
.includes(:owner, :crop, :garden)
.limit(6)
respond_with @planting
end
@@ -37,15 +49,15 @@ class PlantingsController < DataController
def new
@planting = Planting.new(
planted_at: Time.zone.today,
owner: current_member,
garden: current_member.gardens.first
owner: current_member,
garden: current_member.gardens.first
)
@seed = Seed.find_by(slug: params[:seed_id]) if params[:seed_id]
@crop = Crop.approved.find_by(id: params[:crop_id]) || Crop.new
if params[:garden_id]
@planting.garden = Garden.find_by(
owner: current_member,
id: params[:garden_id]
id: params[:garden_id]
)
end

View File

@@ -2,14 +2,34 @@
class SeedsController < DataController
def index
@owner = Member.find_by(slug: params[:member_slug]) if params[:member_slug].present?
@crop = Crop.find_by(slug: params[:crop_slug]) if params[:crop_slug].present?
@planting = Planting.find_by(slug: params[:planting_id]) if params[:planting_id].present?
where = {}
if params[:member_slug].present?
@owner = Member.find_by(slug: params[:member_slug])
where['owner_id'] = @owner.id
end
if params[:crop_slug].present?
@crop = Crop.find_by(slug: params[:crop_slug])
where['crop_id'] = @crop.id
end
if params[:planting_id].present?
@planting = Planting.find_by(slug: params[:planting_id])
where['parent_planting'] = @planting.id
end
@show_all = (params[:all] == '1')
where['finished'] = false unless @show_all
@filename = csv_filename
@seeds = seeds.order(created_at: :desc).includes(:owner, :crop).paginate(page: params[:page])
@seeds = Seed.search(
where: where,
page: params[:page],
limit: 30,
boost_by: [:created_at],
load: false
)
respond_with(@seeds)
end
@@ -22,8 +42,8 @@ class SeedsController < DataController
def new
@seed = Seed.new
if params[:planting_id]
@planting = Planting.find_by(slug: params[:planting_id])
if params[:planting_slug]
@planting = Planting.find_by(slug: params[:planting_slug])
else
@crop = Crop.find_or_initialize_by(id: params[:crop_id])
end
@@ -56,15 +76,6 @@ class SeedsController < DataController
private
def seeds
records = Seed.all
records = records.where(owner: @owner) if @owner.present?
records = records.where(crop: @crop) if @crop.present?
records = records.where(parent_planting: @planting) if @planting.present?
records = records.active unless @show_all
records
end
def seed_params
params.require(:seed).permit(
:crop_id, :description, :quantity, :plant_before,

View File

@@ -74,14 +74,14 @@ module ButtonsHelper
def planting_finish_button(planting, classes: 'btn btn-default btn-secondary')
return unless can?(:edit, planting) || planting.finished
link_to planting_path(planting, planting: { finished: 1 }),
link_to planting_path(slug: planting.slug, planting: { finished: 1 }),
method: :put, class: "#{classes} append-date" do
finished_icon + ' ' + t('buttons.mark_as_finished')
end
end
def seed_finish_button(seed, classes: 'btn btn-default')
return unless can?(:create, Planting) && seed.active?
return unless can?(:create, Planting) && seed.active
link_to seed_path(seed, seed: { finished: 1 }), method: :put, class: "#{classes} append-date" do
finished_icon + ' ' + t('buttons.mark_as_finished')
@@ -89,9 +89,9 @@ module ButtonsHelper
end
def planting_harvest_button(planting, classes: 'btn btn-default')
return unless planting.active? && can?(:create, Harvest) && can?(:edit, planting)
return unless planting.active && can?(:create, Harvest) && can?(:edit, planting)
link_to new_planting_harvest_path(planting), class: classes do
link_to new_planting_harvest_path(planting_slug: planting.slug), class: classes do
harvest_icon + ' ' + t('buttons.record_harvest')
end
end
@@ -99,7 +99,7 @@ module ButtonsHelper
def planting_save_seeds_button(planting, classes: 'btn btn-default')
return unless can?(:edit, planting)
link_to new_planting_seed_path(planting), class: classes do
link_to new_planting_seed_path(planting_slug: planting.slug), class: classes do
seed_icon + ' ' + t('buttons.save_seeds')
end
end

View File

@@ -15,6 +15,6 @@ module CropsHelper
end
def crop_ebay_seeds_url(crop)
"https://rover.ebay.com/rover/1/705-53470-19255-0/1?icep_ff3=9&pub=5575213277&toolid=10001&campid=5337940151&customid=&icep_uq=#{CGI.escape crop.name}&icep_sellerId=&icep_ex_kw=&icep_sortBy=12&icep_catId=181003&icep_minPrice=&icep_maxPrice=&ipn=psmain&icep_vectorid=229515&kwid=902099&mtid=824&kw=lg" # rubocop:disable Metrics/LineLength
"https://rover.ebay.com/rover/1/705-53470-19255-0/1?icep_ff3=9&pub=5575213277&toolid=10001&campid=5337940151&customid=&icep_uq=#{CGI.escape crop.name}&icep_sellerId=&icep_ex_kw=&icep_sortBy=12&icep_catId=181003&icep_minPrice=&icep_maxPrice=&ipn=psmain&icep_vectorid=229515&kwid=902099&mtid=824&kw=lg" # rubocop:disable Layout/LineLength
end
end

View File

@@ -1,41 +0,0 @@
module CropSearch
extend ActiveSupport::Concern
included do
####################################
# Elastic search configuration
searchkick word_start: %i(name alternate_names scientific_names),
case_sensitive: false,
merge_mappings: true,
mappings: {
properties: {
created_at: { type: :integer }
}
}
# Special scope to control if it's in the search index
scope :search_import, -> { includes(:scientific_names, :photos) }
def should_index?
approved?
end
def search_data
{
name: name,
slug: slug,
alternate_names: alternate_names.pluck(:name),
scientific_names: scientific_names.pluck(:name),
# boost the crops that are planted the most
plantings_count: plantings_count,
# boost this crop for these members
planters_ids: plantings.pluck(:owner_id),
has_photos: photos.size.positive?,
photo: default_photo&.thumbnail_url,
scientific_name: default_scientific_name&.name,
description: description,
created_at: created_at.to_i
}
end
end
end

View File

@@ -7,7 +7,7 @@ module Finishable
scope :finished, -> { where(finished: true) }
scope :current, -> { where.not(finished: true) }
def active?
def active
!finished
end
end

View File

@@ -9,6 +9,10 @@ module Likeable
end
def liked_by?(member)
member && members.include?(member)
liked_by_members_names.include?(member.login_name)
end
def liked_by_members_names
Member.where(id: likes.pluck(:member_id)).pluck(:login_name)
end
end

View File

@@ -15,6 +15,14 @@ module PhotoCapable
end
end
def thumbnail_url
df = default_photo
return unless df
df.source == 'flickr' ? df.fullsize_url : df.thumbnail_url
end
def most_liked_photo
photos.order(likes_count: :desc, created_at: :desc).first
end

View File

@@ -0,0 +1,49 @@
# frozen_string_literal: true
module SearchCrops
extend ActiveSupport::Concern
included do
####################################
# Elastic search configuration
searchkick word_start: %i(name description alternate_names scientific_names),
case_sensitive: false,
searchable: %i(name descriptions alternate_names scientific_names),
merge_mappings: true,
mappings: {
properties: {
created_at: { type: :integer },
plantings_count: { type: :integer },
harvests_count: { type: :integer },
photos_count: { type: :integer }
}
}
# Special scope to control if it's in the search index
scope :search_import, -> { includes(:scientific_names, :photos) }
def should_index?
approved?
end
def search_data
{
name: name,
description: description,
slug: slug,
alternate_names: alternate_names.pluck(:name),
scientific_names: scientific_names.pluck(:name),
photos_count: photo_associations_count,
# boost the crops that are planted the most
plantings_count: plantings_count,
harvests_count: harvests_count,
# boost this crop for these members
planters_ids: plantings.pluck(:owner_id),
has_photos: photos.size.positive?,
thumbnail_url: thumbnail_url,
scientific_name: default_scientific_name&.name,
created_at: created_at.to_i
}
end
end
end

View File

@@ -0,0 +1,60 @@
# frozen_string_literal: true
module SearchHarvests
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true, mappings: {
properties: {
harvests_count: { type: :integer },
photos_count: { type: :integer },
created_at: { type: :integer },
harvested_at: { type: :date }
}
}
scope :search_import, -> { includes(:owner, :crop, :plant_part) }
def search_data
{
slug: slug,
quantity: quantity,
# crop
crop_id: crop_id,
crop_name: crop_name,
crop_slug: crop.slug,
# owner
owner_id: owner_id,
owner_login_name: owner_login_name,
owner_slug: owner_slug,
plant_part_name: plant_part&.name,
# planting
planting_id: planting_id,
planting_slug: planting&.slug,
# photo
has_photos: photos.size.positive?,
thumbnail_url: default_photo&.thumbnail_url || crop.default_photo&.thumbnail_url,
# counts
photos_count: photos.count,
# timestamps
harvested_at: harvested_at,
created_at: created_at.to_i
}
end
def self.homepage_records(limit)
search('*', limit: limit,
where: {
photos_count: { gt: 0 }
},
boost_by: [:created_at],
load: false)
end
end
end

View File

@@ -0,0 +1,34 @@
# frozen_string_literal: true
module SearchPhotos
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true, mappings: {
properties: {
title: { type: :text },
created_at: { type: :integer }
}
}
def search_data
{
id: id,
title: title,
thumbnail_url: thumbnail_url,
fullsize_url: fullsize_url,
# crops
crops: crops.pluck(:id),
# likes
liked_by_members_names: liked_by_members_names,
# owner
owner_id: owner_id,
owner_login_name: owner.login_name,
# counts
likes_count: likes_count,
created_at: created_at.to_i
}
end
end
end

View File

@@ -0,0 +1,63 @@
# frozen_string_literal: true
module SearchPlantings
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true, mappings: {
properties: {
active: { type: :boolean },
created_at: { type: :integer },
harvests_count: { type: :integer },
photos_count: { type: :integer },
owner_location: { type: :text }
}
}
scope :search_import, -> { includes(:owner, :crop) }
def search_data
{
slug: slug,
active: active,
finished: finished?,
has_photos: photos.size.positive?,
location: location,
percentage_grown: percentage_grown.to_i,
planted_at: planted_at,
planted_from: planted_from,
quantity: quantity,
sunniness: sunniness,
# crops
crop_id: crop_id,
crop_name: crop.name,
crop_slug: crop.slug,
# owner
owner_id: owner_id,
owner_location: owner_location,
owner_login_name: owner_login_name,
owner_slug: owner_slug,
# photos
thumbnail_url: default_photo&.thumbnail_url || crop.default_photo&.thumbnail_url,
# counts
photos_count: photos.size,
harvests_count: harvests_count,
# timestamps
created_at: created_at.to_i
}
end
def self.homepage_records(limit)
search('*', limit: limit,
where: {
photos_count: { gt: 0 }
},
boost_by: [:created_at],
load: false)
end
end
end

View File

@@ -0,0 +1,66 @@
# frozen_string_literal: true
module SearchSeeds
extend ActiveSupport::Concern
included do
searchkick merge_mappings: true, mappings: {
properties: {
id: { type: :integer },
created_at: { type: :integer },
plant_before: { type: :text },
photos_count: { type: :integer },
tradable_to: { type: :text }
}
}
def search_data
{
slug: slug,
finished: finished?,
gmo: gmo,
active: active,
heirloom: heirloom,
location: owner.location,
organic: organic,
quantity: quantity,
plant_before: plant_before&.to_s(:ymd),
tradable_to: tradable_to,
tradable: tradable,
# crop
crop_id: crop_id,
crop_name: crop.name,
crop_slug: crop.slug,
# owner
owner_id: owner_id,
owner_location: owner_location,
owner_login_name: owner_login_name,
owner_slug: owner_slug,
# planting
parent_planting: parent_planting,
# counts
photos_count: photos.size,
# photo
has_photos: photos.size.positive?,
thumbnail_url: default_photo&.thumbnail_url || crop.default_photo&.thumbnail_url,
created_at: created_at.to_i
}
end
def self.homepage_records(limit)
search('*', limit: limit,
where: {
finished: false,
tradable: true
},
boost_by: [:created_at],
load: false)
end
end
end

View File

@@ -4,7 +4,7 @@ class Crop < ApplicationRecord
extend FriendlyId
include PhotoCapable
include OpenFarmData
include CropSearch
include SearchCrops
friendly_id :name, use: %i(slugged finders)
@@ -18,7 +18,7 @@ class Crop < ApplicationRecord
has_many :plantings, dependent: :destroy
has_many :seeds, dependent: :destroy
has_many :harvests, dependent: :destroy
has_many :photo_associations, dependent: :delete_all
has_many :photo_associations, dependent: :delete_all, inverse_of: :crop
has_many :photos, through: :photo_associations
has_many :plant_parts, -> { joins_members.distinct.order("plant_parts.name") }, through: :harvests
has_many :varieties, class_name: 'Crop', foreign_key: 'parent_id', dependent: :nullify, inverse_of: :parent

View File

@@ -77,6 +77,8 @@ class Garden < ApplicationRecord
end
end
def reindex(refresh: false); end
protected
def strip_blanks

View File

@@ -5,6 +5,7 @@ class Harvest < ApplicationRecord
extend FriendlyId
include PhotoCapable
include Ownable
include SearchHarvests
friendly_id :harvest_slug, use: %i(slugged finders)
@@ -45,10 +46,14 @@ class Harvest < ApplicationRecord
scope :recent, -> { order(created_at: :desc) }
scope :one_per_owner, lambda {
joins("JOIN members m ON (m.id=harvests.owner_id)
LEFT OUTER JOIN harvests h2
ON (m.id=h2.owner_id AND harvests.id < h2.id)").where("h2 IS NULL")
LEFT OUTER JOIN harvests h2
ON (m.id=h2.owner_id AND harvests.id < h2.id)").where("h2 IS NULL")
}
delegate :name, :slug, to: :crop, prefix: true
delegate :login_name, :slug, to: :owner, prefix: true
delegate :name, to: :plant_part, prefix: true
##
## Validations
validates :crop, approved: true

View File

@@ -3,11 +3,18 @@
class Photo < ApplicationRecord
include Likeable
include Ownable
include SearchPhotos
PHOTO_CAPABLE = %w(Garden Planting Harvest Seed Post Crop).freeze
has_many :photo_associations, foreign_key: :photo_id, dependent: :delete_all, inverse_of: :photo
has_many :crops, through: :photo_associations, counter_cache: true
# This doesn't work, ActiveRecord tries to use the polymoriphinc photographable
# relationship instead.
# has_many :crops, through: :photo_associations
def crops
Crop.distinct.joins(:photo_associations).where(photo_associations: { photo: self })
end
validates :fullsize_url, url: true
validates :thumbnail_url, url: true
@@ -25,6 +32,8 @@ class Photo < ApplicationRecord
joins(:photo_associations).where(photo_associations: { photographable_type: model_name.to_s })
}
delegate :login_name, to: :owner, prefix: true
# This is split into a side-effect free method and a side-effecting method
# for easier stubbing and testing.
def flickr_metadata

View File

@@ -1,24 +1,23 @@
# frozen_string_literal: true
class PhotoAssociation < ApplicationRecord
belongs_to :photo, inverse_of: :photo_associations
belongs_to :photo, touch: true
belongs_to :crop, optional: true, touch: true # , counter_cache: true
belongs_to :photographable, polymorphic: true, touch: true
belongs_to :crop, optional: true, counter_cache: true
validate :photo_and_item_have_same_owner
validate :crop_present
##
## Triggers
before_save :set_crop
def item
find_by!(photographable_id: photographable_id, photographable_type: photographable_type).photographable
end
before_validation :set_crop
def self.item(item_id, item_type)
find_by!(photographable_id: item_id, photographable_type: item_type).photographable
end
private
def set_crop
if %w(Planting Seed Harvest).include?(photographable_type)
self.crop_id = photographable.crop_id
@@ -27,11 +26,15 @@ class PhotoAssociation < ApplicationRecord
end
end
private
def photo_and_item_have_same_owner
return unless photographable_type != 'Crop'
return if photographable_type == 'Crop'
errors.add(:photo, "must have same owner as item it links to") unless photographable.owner_id == photo.owner_id
end
def crop_present
if %w(Planting Seed Harvest).include?(photographable_type)
errors.add(:crop_id, "failed to calculate crop") if crop_id.blank?
end
end
end

View File

@@ -7,6 +7,8 @@ class Planting < ApplicationRecord
include Ownable
include PredictPlanting
include PredictHarvest
include SearchPlantings
friendly_id :planting_slug, use: %i(slugged finders)
# Constants
@@ -56,6 +58,8 @@ class Planting < ApplicationRecord
## Delegations
delegate :name, :slug, :en_wikipedia_url, :default_scientific_name, :plantings_count,
to: :crop, prefix: true
delegate :login_name, :slug, :location, to: :owner, prefix: true
delegate :slug, to: :planting, prefix: true
delegate :annual?, :perennial?, :svg_icon, to: :crop
delegate :location, :longitude, :latitude, to: :garden

View File

@@ -57,6 +57,8 @@ class Post < ApplicationRecord
subject
end
def reindex(refresh: false); end
private
def update_crop_posts_association

View File

@@ -5,6 +5,7 @@ class Seed < ApplicationRecord
include PhotoCapable
include Finishable
include Ownable
include SearchSeeds
friendly_id :seed_slug, use: %i(slugged finders)
TRADABLE_TO_VALUES = %w(nowhere locally nationally internationally).freeze
@@ -46,7 +47,9 @@ class Seed < ApplicationRecord
#
# Delegations
delegate :name, to: :crop
delegate :name, to: :crop, prefix: true
delegate :location, :latitude, :longitude, to: :owner
delegate :login_name, :slug, :location, to: :owner, prefix: true
#
# Scopes
@@ -57,7 +60,7 @@ class Seed < ApplicationRecord
scope :recent, -> { order(created_at: :desc) }
scope :active, -> { where('finished <> true').where('finished_at IS NULL OR finished_at < ?', Time.zone.now) }
def tradable?
def tradable
tradable_to != 'nowhere'
end

View File

@@ -4,13 +4,14 @@ class CropSearchService
# Crop.search(string)
def self.search(query, page: 1, per_page: 12, current_member: nil)
search_params = {
page: page,
per_page: per_page,
fields: %i(name^5 alternate_names scientific_names),
match: :word_start,
boost_by: [:plantings_count],
includes: %i(scientific_names alternate_names),
misspellings: { edit_distance: 2 }
page: page,
per_page: per_page,
fields: %i(name^5 alternate_names scientific_names),
match: :word_start,
boost_by: [:plantings_count],
includes: %i(scientific_names alternate_names),
misspellings: { edit_distance: 2 },
load: false
}
# prioritise crops the member has planted
search_params[:boost_where] = { planters_ids: current_member.id } if current_member
@@ -22,22 +23,22 @@ class CropSearchService
body = {
"query": {
"function_score": {
"query": { "query_string": { "query": 'has_photos:true' } },
"query": { "query_string": { "query": 'has_photos:true' } },
"random_score": { "seed": DateTime.now.to_i }
}
}
}
Crop.search(
limit: limit,
load: false,
body: body
load: false,
body: body
)
end
def self.recent(limit)
Crop.search(
limit: limit,
load: false,
limit: limit,
load: false,
boost_by: { created_at: { factor: 100 } } # default factor is 1
)
end

View File

@@ -6,7 +6,7 @@
crop
.card-body
%h3.card-title
%strong= link_to crop, crop
%strong= link_to crop.name, crop_path(slug: crop.slug)
= crop.default_scientific_name
.d-flex.justify-content-between
- if crop.annual? && crop.median_lifespan.present?

View File

@@ -1,7 +1,7 @@
- if photos.size.positive?
- if crop.photos.count.positive?
%h2 #{photo_icon} Photos
- [Crop, Planting, Harvest, Seed].each do |model_name|
- if photos.by_model(model_name).size.positive?
- if crop.photos.by_model(model_name).size.positive?
%h3 #{@crop.name.capitalize} #{t("activerecord.models.#{model_name.to_s.downcase}.other")}
= render 'photos/gallery', photos: photos.by_model(model_name).includes(:owner).order(likes_count: :desc).limit(5)
= render 'photos/gallery', photos: crop.photos.by_model(model_name).includes(:owner).order(likes_count: :desc).limit(5)
= link_to 'more photos »', crop_photos_path(@crop), class: 'btn'

View File

@@ -1,12 +1,11 @@
- cache crop do
.card.crop-thumbnail
= link_to image_tag(crop_image_path(crop),
= link_to image_tag(crop.thumbnail_url ? crop.thumbnail_url : placeholder_image,
alt: crop.name,
class: 'img img-card'),
crop
crop_path(slug: crop.slug)
.text
%h3.crop-name= link_to crop, crop
%h3.crop-name= link_to crop.name, crop_path(slug: crop.slug)
%h5.crop-sci-name
&nbsp;
= crop.scientific_names.first&.name
= crop.scientific_names.first

View File

@@ -22,8 +22,8 @@
%h2= t('.title')
= will_paginate @crops
.index-cards
- @crops.each do |crop|
= render 'crops/thumbnail', crop: crop
- @crops.each do |c|
= render 'crops/thumbnail', crop: c
= will_paginate @crops

View File

@@ -32,7 +32,7 @@
%section.photos
= cute_icon
= render 'crops/photos', photos: @photos, crop: @crop
= render 'crops/photos', crop: @crop
- if @crop.plantings.any?
%section.charts

View File

@@ -1,11 +1,13 @@
.card
= link_to harvest do
= image_tag harvest_image_path(harvest), alt: harvest, class: 'img-card'
.card-body
%h5
= crop_icon(harvest.crop)
%strong
= link_to harvest.crop, harvest
%span.badge.badge-pill= harvest.plant_part
.card-footer
.float-right=render 'members/tiny', member: harvest.owner
- cache harvest do
.card
= link_to harvest do
= image_tag harvest.thumbnail_url ? harvest.thumbnail_url : placeholder_image, alt: harvest.crop_name, class: 'img-card'
.card-body
%h5
%strong= link_to harvest.crop_name, harvest_path(slug: harvest.slug)
%span.badge.badge-pill= harvest.plant_part_name
.card-footer
.float-right
%span.chip.member-chip
= link_to member_path(slug: harvest.owner_slug) do
= harvest.owner_login_name

View File

@@ -1,6 +1,4 @@
- if local_assigns[:full]
- cache harvest do
= render 'harvests/card', harvest: harvest
= render 'harvests/card', harvest: harvest
- else
- cache harvest do
= render 'harvests/thumbnail', harvest: harvest
= render 'harvests/thumbnail', harvest: harvest

View File

@@ -1,9 +1,10 @@
.card.harvest-thumbnail
= link_to image_tag(harvest_image_path(harvest),
alt: harvest,
class: 'img img-card'),
harvest
- cache harvest do
.card.harvest-thumbnail
= link_to image_tag(harvest_image_path(harvest),
alt: harvest,
class: 'img img-card'),
harvest
.text
%h3.harvest-plant-part= link_to harvest.plant_part, harvest
%h5.harvest-crop= harvest.crop
.text
%h3.harvest-plant-part= link_to harvest.plant_part, harvest
%h5.harvest-crop= harvest.crop

View File

@@ -29,8 +29,9 @@
.badge.badge-success= link_to 'API Methods', '/api-docs'
.col-md-10
%section
%h2
= page_entries_info @harvests
%h2= page_entries_info @harvests
= will_paginate @harvests
.index-cards=render @harvests, full: true
.index-cards
- @harvests.each do |h|
= render 'harvests/card', harvest: h
= will_paginate @harvests

View File

@@ -2,18 +2,17 @@
%rss{ version: 2.0 }
%channel
%title
Recent harvests from #{@owner ? @owner : 'all members'} (#{ENV['GROWSTUFF_SITE_NAME']})
Recent harvests from #{@owner ||= 'all members'} (#{ENV['GROWSTUFF_SITE_NAME']})
%link= harvests_url
- @harvests.each do |harvest|
%item
%title #{harvest.owner.login_name}'s #{harvest.crop.name}
%pubdate= harvest.harvested_at.to_s(:rfc822)
%title #{harvest.owner_login_name}'s #{harvest.crop_name}
%pubdate= harvest.harvested_at
%description
:escaped
<p>Crop: #{harvest.crop ? harvest.crop : 'unknown' }</p>
<p>Quantity: #{harvest.quantity ? harvest.quantity : 'unknown' }</p>
<p>Harvested on: #{harvest.harvested_at ? harvest.harvested_at : 'unknown' }</p>
:escaped_markdown
#{ strip_tags harvest.description }
%link= harvest_url(harvest)
%guid= harvest_url(harvest)
<p>Crop: #{harvest.crop_name}</p>
<p>Plant path: #{harvest.plant_part_name}</p>
<p>Quantity: #{harvest.quantity ||= 'unknown' }</p>
<p>Harvested on: #{harvest.harvested_at ||= 'unknown' }</p>
%link= harvest_url(slug: harvest.slug)
%guid= harvest_url(slug: harvest.slug)

View File

@@ -1,16 +1,4 @@
- cache cache_key_for(Crop, 'homepage'), expires_in: 1.day do
.index-cards
- CropSearchService.random_with_photos(16).each do |crop|
.card.crop-thumbnail
= link_to crop_path(slug: crop['slug']) do
= image_tag(crop['photo'],
alt: crop['name'],
class: 'img img-card')
.text
%h3.crop-name
= link_to crop['name'], crop_path(slug: crop['slug'])
%h5.crop-sci-name
&nbsp;
= crop['scientific_name']
- CropSearchService.random_with_photos(16).each do |c|
= render 'crops/thumbnail', crop: c

View File

@@ -12,7 +12,7 @@
%p.mb-2
= truncate(strip_tags(post.body), length: 200)
%small
= post.comments.size
= post.comments_count
comments
%p.text-right
= link_to "#{t('.view_all')} »", posts_path, class: 'btn btn-block'

View File

@@ -1,11 +1,12 @@
%h2= t('.recently_harvested')
- Harvest.has_photos.recent.includes(:crop, :owner, :photos, :plant_part).limit(6).each do |harvest|
- Harvest.homepage_records(6).each do |harvest|
- cache harvest do
= link_to harvest, class: 'list-group-item list-group-item-action flex-column align-items-start' do
= link_to harvest_path(slug: harvest.slug), class: 'list-group-item list-group-item-action flex-column align-items-start' do
.d-flex.w-100.justify-content-between.homepage--list-item
%div
%h5= harvest.crop.name
%h5= harvest.crop_name
%span.badge.badge-success=harvest.plant_part
%small.text-muted
harvested by #{harvest.owner}
%p.mb-2= image_tag harvest_image_path(harvest), width: 75, class: 'rounded shadow'
harvested by #{harvest.owner_name}
%p.mb-2
= image_tag harvest.thumbnail_url, width: 75, class: 'rounded shadow'

View File

@@ -1,12 +1,11 @@
%h2= t('.recently_planted')
- Planting.has_photos.recent.includes(:owner, :crop).limit(6).each do |planting|
- cache planting do
= link_to planting, class: 'list-group-item list-group-item-action flex-column align-items-start' do
.d-flex.w-100.justify-content-between.homepage--list-item
%p.mb-2
= image_tag planting_image_path(planting), width: 75, class: 'rounded shadow'
.text-right
%h5= planting.crop.name
- if planting.planted_from.present?
%span.badge.badge-success= planting.planted_from.pluralize
%small.text-muted planted by #{planting.owner}
- Planting.homepage_records(6).each do |planting|
= link_to planting_path(slug: planting['slug']), class: 'list-group-item list-group-item-action flex-column align-items-start' do
.d-flex.w-100.justify-content-between.homepage--list-item
%p.mb-2
= image_tag planting['thumbnail_url'], width: 75, class: 'rounded shadow'
.text-right
%h5= planting['crop_name']
- if planting['planted_from'].present?
%span.badge.badge-success= planting['planted_from'].pluralize
%small.text-muted planted by #{planting['owner_name']}

View File

@@ -1,5 +1,5 @@
- cache cache_key_for(Seed) do
%h2.text-center= t('home.seeds.title')
.index-cards
- Seed.current.tradable.includes(:owner, :crop).order(created_at: :desc).limit(6).each do |seed|
= render 'seeds/card', seed: seed
- Seed.homepage_records(6).each do |s|
= render 'seeds/card', seed: s

View File

@@ -5,7 +5,7 @@
= link_to timeline_index_path, method: :get, class: 'nav-link text-white' do
= image_tag 'icons/notification.svg', class: 'img img-icon'
%li.nav-item
= link_to member_gardens_path(member_slug: current_member.slug), class: 'nav-link text-white' do
= link_to member_gardens_path(current_member), class: 'nav-link text-white' do
= image_icon 'gardens'
%li.nav-item.dropdown
%a.nav-link.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", href: "#", role: "button"}

View File

@@ -1,4 +1,4 @@
%span.badge.like-badge{class: (likeable.liked_by?(current_member)) ? 'liked' : ''}
%span.badge.like-badge{class: liked ? 'liked' : ''}
= like_icon
&nbsp;
%span.like-count= likeable.likes_count

View File

@@ -1,10 +1,11 @@
.card.photo-card{id: "photo-#{photo.id}"}
= link_to image_tag(photo.source == 'flickr' ? photo.fullsize_url : photo.thumbnail_url, alt: photo.title, class: 'img img-card'), photo
= link_to photo_path(id: photo.id) do
= image_tag(photo.source == 'flickr' ? photo.fullsize_url : photo.thumbnail_url, alt: photo.title, class: 'img img-card')
.card-body
%h5.ellipsis
= photo_icon
= link_to photo.title, photo
%i by #{link_to photo.owner, photo.owner}
= link_to photo.title, photo_path(id: photo.id)
%i by #{link_to photo.owner_login_name, member_path(slug: photo.owner_login_name)}
- if photo.date_taken.present?
%small.text-muted
%time{datetime: photo.date_taken}= I18n.l(photo.date_taken.to_date)

View File

@@ -1,14 +1,13 @@
%span.likes
- if member_signed_in?
- if can?(:new, Like) && !photo.liked_by?(current_member)
= link_to likes_path(photo_id: photo.id, format: :json),
method: :post, remote: true, class: 'photo-like like-btn' do
= render 'likes/count', likeable: photo
- if member_signed_in? && can?(:new, Like)
- if !photo.liked_by_members_names.include?(current_member.login_name)
= link_to likes_path(type: 'Photo', id: photo.id, format: :json),
method: :post, remote: true, class: 'photo-like like-btn ' do
= render 'likes/count', likeable: photo, liked: false
- else
- like = photo.likes.find_by(member: current_member)
- if like && can?(:destroy, like)
= link_to like_path(id: like.id, format: :json),
method: :delete, remote: true, class: 'photo-like like-btn' do
= render 'likes/count', likeable: photo
= link_to likes_path(type: 'Photo', id: photo.id, format: :json),
method: :delete, remote: true, class: 'photo-like like-btn' do
= render 'likes/count', likeable: photo, liked: true
- else
= render 'likes/count', likeable: photo
= render 'likes/count', likeable: photo, liked: member_signed_in? && photo.liked_by_members_names.include?(current_member.login_name)

View File

@@ -4,7 +4,7 @@
.dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "planting-actions-button"}
= planting_edit_button(planting, classes: 'dropdown-item')
= add_photo_button(planting, classes: 'dropdown-item')
- if planting.active?
- if planting.active
= planting_finish_button(planting, classes: 'dropdown-item')
= planting_harvest_button(planting, classes: 'dropdown-item')
= planting_save_seeds_button(planting, classes: 'dropdown-item')

View File

@@ -1,27 +1,37 @@
- cache planting do
.card.planting{class: planting.active? ? '' : 'card-finished'}
= link_to planting do
= image_tag planting_image_path(planting), class: 'img-card', alt: planting
- if can? :edit, planting
.planting-quick-actions
.dropdown
%a.planting-menu.btn.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", type: "button", href: '#'}
.dropdown-menu{"aria-labelledby" => "planting-menu"}
= planting_edit_button(planting, classes: 'dropdown-item')
= add_photo_button(planting, classes: 'dropdown-item')
.card.planting{class: planting.active ? '' : 'card-finished'}
= link_to planting_path(slug: planting.slug) do
= image_tag planting.thumbnail_url ? planting.thumbnail_url : placeholder_image, class: 'img-card', alt: planting.crop_name
- if planting.active?
= planting_finish_button(planting, classes: 'dropdown-item')
= planting_harvest_button(planting, classes: 'dropdown-item')
= planting_save_seeds_button(planting, classes: 'dropdown-item')
- if member_signed_in? && current_member.id == planting.owner_id
= link_to planting_path(slug: planting.slug) do
.planting-quick-actions
.dropdown
%a.planting-menu.btn.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", type: "button", href: '#'}
- if can? :destroy, planting
.dropdown-divider
= delete_button(planting, classes: 'dropdown-item text-danger')
= link_to planting do
.dropdown-menu{"aria-labelledby" => "planting-menu"}
= link_to edit_planting_path(slug: planting.slug), class: 'dropdown-item' do
= edit_icon
= t('buttons.edit')
= link_to new_photo_path(id: planting.id, type: 'planting'), class: 'dropdown-item' do
= add_photo_icon
= t('buttons.add_photo')
- if planting.active
= planting_finish_button(planting, classes: 'dropdown-item')
= planting_harvest_button(planting, classes: 'dropdown-item')
= planting_save_seeds_button(planting, classes: 'dropdown-item')
- if can? :destroy, planting
.dropdown-divider
= delete_button(planting, classes: 'dropdown-item text-danger')
= link_to planting_path(slug: planting.slug) do
.card-body.text-center
%h4= planting.crop
%h4= planting.crop_name
.text-center= render 'plantings/badges', planting: planting
= render 'plantings/progress', planting: planting
.card-footer
.float-right=render 'members/tiny', member: planting.owner
.float-right
%span.chip.member-chip
= link_to member_path(slug: planting.owner_slug) do
= planting.owner_login_name

View File

@@ -16,14 +16,14 @@
unknown
- if planting.planted_at.present?
%span.planted_at
=planting.planted_at.year
= planting.planted_at.year
- if planting.finish_is_predicatable?
.card.fact-card
%h3 Progress
%strong #{planting.age_in_days}/#{planting.expected_lifespan}
%span days
.card.fact-card{class: planting.quantity.present? ? '' : 'text-muted'}
%h3
Quantity

View File

@@ -1,13 +1,13 @@
%h2 Harvests
- if can? :edit, @planting
- if can? :edit, planting
%a.btn.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", :href => "#", :role => "button"}
= harvest_icon
Record harvest
.dropdown-menu.dropdown-secondary
- PlantPart.all.each do |plant_part|
= link_to harvests_path(return: 'planting', harvest: {crop_id: @planting.crop_id, planting_id: @planting.id, plant_part_id: plant_part.id}), method: :post, class: 'dropdown-item' do
= link_to harvests_path(return: 'planting', harvest: {crop_id: planting.crop_id, planting_id: planting.id, plant_part_id: plant_part.id}), method: :post, class: 'dropdown-item' do
= plant_part.name
- if planting.harvests.empty?
@@ -16,5 +16,5 @@
%p Record your harvests here to improve crop predictions, and you'll be able to compare with your garden next season.
- else
.index-cards
- planting.harvests.order(created_at: :desc).includes(:crop).each do |harvest|
- planting.harvests.each do |harvest|
= render 'harvests/thumbnail', harvest: harvest

View File

@@ -1,4 +1,4 @@
- if planting.active? && planting.annual? && planting.percentage_grown.present?
- if planting.active && planting.annual? && planting.percentage_grown.present?
.progress
.progress-bar.bg-success{"aria-valuemax" => "100", "aria-valuemin" => "0", "aria-valuenow" => planting.percentage_grown, role: "progressbar", style: "width: #{planting.percentage_grown}%"}

View File

@@ -6,7 +6,7 @@
%li= planting_edit_button(planting, classes: 'dropdown-item')
%li= add_photo_button(planting, classes: 'dropdown-item')
- if planting.active?
- if planting.active
%li= planting_finish_button(planting, classes: 'dropdown-item')
%li= planting_harvest_button(planting, classes: 'dropdown-item')
%li= planting_save_seeds_button(planting, classes: 'dropdown-item')

View File

@@ -10,7 +10,6 @@
%h1.display-2.text-center
= planting_icon
= title('plantings', @owner, @crop, @planting)
.row
.col-md-2
= render 'layouts/nav', model: Planting
@@ -20,9 +19,9 @@
include finished plantings
%hr
- if @owner.present?
= render @owner
= render @owner, cached: true
- if @crop.present?
= render @crop
= render @crop, cached: true
%section.open-data
%h2 Open Data
@@ -40,7 +39,6 @@
%h2= page_entries_info @plantings
= will_paginate @plantings
.index-cards
- @plantings.each do |planting|
= render planting, full: true
- @plantings.each do |p|
= render 'plantings/card', planting: p
= will_paginate @plantings

View File

@@ -6,15 +6,15 @@
%link= plantings_url
- @plantings.each do |planting|
%item
%title #{planting.crop} in #{planting.location}
%pubdate= planting.created_at.to_s(:rfc822)
%title #{planting['crop_name']} in #{planting['location']}
%pubdate= planting['created_at'].to_s(:rfc822)
%description
:escaped
<p>Quantity: #{planting.quantity ? planting.quantity : 'unknown' }</p>
<p>Planted on: #{planting.planted_at ? planting.planted_at : 'unknown' }</p>
<p>Sunniness: #{planting.sunniness ? planting.sunniness : 'unknown' }</p>
<p>Planted from: #{planting.planted_from ? planting.planted_from : 'unknown' }</p>
<p>Quantity: #{planting['quantity'] ? planting['quantity'] : 'unknown' }</p>
<p>Planted on: #{planting['planted_at'] ? planting['planted_at'] : 'unknown' }</p>
<p>Sunniness: #{planting['sunniness'] ? planting['sunniness'] : 'unknown' }</p>
<p>Planted from: #{planting['planted_from'] ? planting['planted_from'] : 'unknown' }</p>
:escaped_markdown
#{ strip_tags planting.description }
%link= planting_url(planting)
%guid= planting_url(planting)
#{ strip_tags planting['description'] }
%link= planting_url(slug: planting['slug'])
%guid= planting_url(slug: planting['slug'])

View File

@@ -1,12 +1,10 @@
- if member_signed_in?
- if can?(:new, Like) && !post.liked_by?(current_member)
= link_to 'Like', likes_path(post_id: post.id, format: :json),
- if member_signed_in? && can?(:new, Like)
- if !post.liked_by?(current_member)
= link_to 'Like', likes_path(type: 'Post', id: post.id, format: :json),
method: :post, remote: true, class: 'post-like btn like-btn'
- else
- like = post.likes.find_by(member: current_member)
- if like && can?(:destroy, like)
= link_to 'Unlike', like_path(id: like.id, format: :json),
method: :delete, remote: true, class: 'post-like btn like-btn'
= link_to 'Unlike', likes_path(type: 'Post', id: post.id, format: :json),
method: :delete, remote: true, class: 'post-like btn like-btn'
= render 'likes/count', likeable: post
= render 'likes/count', likeable: post, liked: member_signed_in? && post.liked_by?(current_member)

View File

@@ -3,13 +3,13 @@
%a#seed-actions-button.btn.btn-info.dropdown-toggle{"aria-expanded" => "false", "aria-haspopup" => "true", "data-toggle" => "dropdown", type: "button", href: '#'}
Actions
.dropdown-menu.dropdown-menu-xs{"aria-labelledby" => "seed-actions-button"}
- if can?(:create, Planting) && can?(:update, seed) && seed.active?
- if can?(:create, Planting) && can?(:update, seed) && seed.active
= link_to new_planting_path(seed_id: seed), class: 'dropdown-item success-color' do
= seed_icon
Plant seeds
= seed_edit_button(seed, classes: 'dropdown-item')
= add_photo_button(seed, classes: 'dropdown-item')
- if seed.active?
- if seed.active
= seed_finish_button(seed, classes: 'dropdown-item')
.dropdown-divider
= delete_button(seed, classes: 'dropdown-item text-danger')

View File

@@ -1,17 +1,17 @@
- cache seed do
.card.seed-card{class: seed.active? ? '' : 'card-finished'}
= link_to seed do
= image_tag(seed_image_path(seed), alt: seed, class: 'img-card')
.card.seed-card{class: seed.finished ? 'card-finished' : ''}
= link_to seed_path(slug: seed.slug) do
= image_tag(seed.thumbnail_url ? seed.thumbnail_url : placeholder_image, alt: seed.crop_name, class: 'img-card')
.text
= render 'members/tiny', member: seed.owner
%span.chip.member-chip
= seed.owner_login_name
.card-body
.card-title
= crop_icon(seed.crop)
= link_to seed.crop, seed
- if seed.tradable?
= link_to seed.crop_name, seed_path(slug: seed.slug)
- if seed.tradable
.text-muted
= icon 'fas', 'map'
Will trade #{seed.tradable_to}
.badge.badge-pill.badge-location
= icon 'fas', 'map-marker'
= truncate(seed.owner.location, length: 20, separator: ' ', omission: '... ')
= truncate(seed.owner_location, length: 20, separator: ' ', omission: '... ')

View File

@@ -38,9 +38,7 @@
= will_paginate @seeds
.index-cards
- @seeds.each do |seed|
= render 'seeds/card', seed: seed
- @seeds.each do |s|
= render 'seeds/card', seed: s
= will_paginate @seeds

View File

@@ -2,24 +2,21 @@
%rss{ version: 2.0 }
%channel
%title
Recent seeds from #{@owner ? @owner : 'all members'} (#{ENV['GROWSTUFF_SITE_NAME']})
Recent seeds from #{@owner ||= 'all members'} (#{ENV['GROWSTUFF_SITE_NAME']})
%link= seeds_url
- @seeds.each do |seed|
%item
%title #{seed.owner}'s #{seed.crop} seeds
%title #{seed.owner_login_name}'s #{seed.crop_name} seeds
%pubdate= seed.created_at.to_s(:rfc822)
%description
:escaped
<p>Quantity: #{seed.quantity ? seed.quantity : 'unknown' }</p>
<p>Plant before: #{seed.plant_before ? seed.plant_before : 'unknown' }</p>
<p>Quantity: #{seed.quantity ||= 'unknown' }</p>
<p>Plant before: #{seed.plant_before ||= 'unknown' }</p>
<p>Organic? #{seed.organic}</p>
<p>GMO? #{seed.gmo}</p>
<p>Heirloom? #{seed.heirloom}</p>
- if seed.tradable?
%p
Will trade #{seed.tradable_to} from #{seed.owner.location ? seed.owner.location : 'unknown location'}
:escaped_markdown
#{ strip_tags seed.description }
%link= seed_url(seed)
%guid= seed_url(seed)
- if seed.tradable
:escaped
<p>Will trade #{seed.tradable_to} from #{seed.location ||= 'unknown location'}</p>
%link= seed_url(slug: seed.slug)
%guid= seed_url(slug: seed.slug)

View File

@@ -50,7 +50,7 @@
#{strip_tags(@seed.description)}
- if current_member
- if @seed.tradable? && current_member != @seed.owner
- if @seed.tradable && current_member != @seed.owner
%p= link_to "Request seeds",
new_message_path(recipient_id: @seed.owner.id,
subject: "Interested in your #{@seed.crop} seeds"),

View File

@@ -125,7 +125,7 @@ en:
title:
crop_harvests: Everyone's %{crop} harvests
default: Everyone's harvests
owner_harvests: "%{owner} harvests"
owner_harvests: "%{owner}'s harvests"
planting_harvests: Harvests from %{planting}
updated: Harvest was successfully updated.
home:

View File

@@ -86,7 +86,10 @@ Rails.application.routes.draw do
resources :forums
resources :follows, only: %i(create destroy)
resources :likes, only: %i(create destroy)
post 'likes' => 'likes#create'
delete 'likes' => 'likes#destroy'
resources :timeline
resources :members, param: :slug do

View File

@@ -30,7 +30,9 @@ class CmsTags < ActiveRecord::Migration[5.2]
layout.content = layout.content.gsub(/\{\{ ?cms:(\w+):([\w]+):([^:]*) ?\}\}/, '{{ cms:\1 \2, "\3" }}') if layout.content.is_a? String
layout.content = layout.content.gsub(/cms:rich_text/, 'cms:wysiwyg') if layout.content.is_a? String
layout.content = layout.content.gsub(/cms:integer/, 'cms:number') if layout.content.is_a? String
layout.content = layout.content.gsub(/cms: string/, 'cms:text') if layout.content.is_a? String # probably a result of goofing one of the more general regexps
if layout.content.is_a? String
layout.content = layout.content.gsub(/cms: string/, 'cms:text')
end # probably a result of goofing one of the more general regexps
if layout.content.is_a? String
layout.content = layout.content.gsub(%r{\{\{ ?cms:page_file ([\w/]+) ?\}\}}, '{{ cms:file \1, render: false }}')
end

View File

@@ -1,7 +1,5 @@
# frozen_string_literal: true
class CropPhotoCounterCache < ActiveRecord::Migration[5.2]
def change
change_table :crops do |t|

View File

@@ -0,0 +1,18 @@
# frozen_string_literal: true
class ElasticIndexing < ActiveRecord::Migration[5.2]
def up
say 'indexing crops'
Crop.reindex
say 'indexing plantings'
Planting.reindex
say 'indexing seeds'
Seed.reindex
say 'indexing harvests'
Harvest.reindex
say 'indexing photos'
Photo.reindex
end
def down; end
end

View File

@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
ActiveRecord::Schema.define(version: 2019_12_26_025225) do
ActiveRecord::Schema.define(version: 2019_12_26_051019) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"

View File

@@ -2,15 +2,15 @@
require 'rails_helper'
describe HarvestsController do
describe HarvestsController, :search do
login_member
def valid_attributes
{
owner_id: subject.current_member.id,
crop_id: FactoryBot.create(:crop).id,
owner_id: subject.current_member.id,
crop_id: FactoryBot.create(:crop).id,
plant_part_id: FactoryBot.create(:plant_part).id,
harvested_at: '2017-01-01'
harvested_at: '2017-01-01'
}
end
@@ -22,24 +22,30 @@ describe HarvestsController do
let!(:harvest1) { FactoryBot.create(:harvest, owner_id: member1.id, crop_id: tomato.id) }
let!(:harvest2) { FactoryBot.create(:harvest, owner_id: member2.id, crop_id: maize.id) }
before { Harvest.reindex }
describe "assigns all harvests as @harvests" do
before { get :index, params: {} }
it { expect(assigns(:harvests)).to eq [harvest1, harvest2] }
it { expect(assigns(:harvests).size).to eq 2 }
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: member1.slug } }
it { expect(assigns(:owner)).to eq member1 }
it { expect(assigns(:harvests)).to eq [harvest1] }
it { expect(assigns(:harvests).size).to eq 1 }
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
before { get :index, params: { crop_slug: maize.name } }
it { expect(assigns(:crop)).to eq maize }
it { expect(assigns(:harvests)).to eq [harvest2] }
it { expect(assigns(:harvests).size).to eq 1 }
it { expect(assigns(:harvests)[0].slug).to eq harvest2.slug }
end
describe "generates a csv" do
@@ -189,8 +195,10 @@ describe HarvestsController do
describe "does not save planting_id" do
before do
put :update, params: { slug: harvest.to_param,
harvest: valid_attributes.merge(planting_id: not_my_planting.id) }
put :update, params: {
slug: harvest.to_param,
harvest: valid_attributes.merge(planting_id: not_my_planting.id)
}
end
it { expect(harvest.planting_id).to eq(nil) }

View File

@@ -10,7 +10,7 @@ describe LikesController do
before { sign_in member }
describe "POST create" do
before { post :create, params: { post_id: blogpost.id, format: :json } }
before { post :create, params: { type: 'Post', id: blogpost.id, format: :json } }
it { expect(response.content_type).to eq "application/json" }
@@ -27,7 +27,7 @@ describe LikesController do
end
describe "DELETE destroy" do
before { delete :destroy, params: { id: like.id, format: :json } }
before { delete :destroy, params: { type: like.likeable_type, id: like.likeable_id, format: :json } }
it { expect(response.content_type).to eq "application/json" }

View File

@@ -2,36 +2,53 @@
require 'rails_helper'
describe PhotosController do
describe PhotosController, :search do
login_member
describe 'GET index' do
describe 'all photos' do
let!(:photo) { FactoryBot.create :photo }
let!(:photo) { FactoryBot.create :photo, :reindex }
before { get :index }
before do
Photo.reindex
get :index
end
it { expect(assigns(:photos)).to eq [photo] }
it "finds photos" do
expect(assigns(:photos).count).to eq 1
expect(assigns(:photos).first.id).to eq photo.id
end
end
describe 'crop photos' do
let!(:photo) { FactoryBot.create :photo, owner: member }
let!(:crop_photo) { FactoryBot.create :photo, owner: member }
let!(:planting) { FactoryBot.create :planting, crop: crop, owner: member }
let!(:crop) { FactoryBot.create :crop }
describe '#index crop photos' do
let!(:photo) { FactoryBot.create :photo, :reindex, owner: member, title: 'no assocations photo' }
let!(:crop_photo) { FactoryBot.create :photo, :reindex, owner: member, title: 'photos of planting' }
let!(:planting) { FactoryBot.create :planting, :reindex, crop: crop, owner: member }
let!(:crop) { FactoryBot.create :crop, :reindex }
before do
planting.photos << crop_photo
Photo.reindex
get :index, params: { crop_slug: crop.to_param }
end
it { expect(assigns(:crop)).to eq crop }
it { expect(assigns(:photos)).to eq [crop_photo] }
describe "find photos by crop" do
it "has indexed the photos of this crop" do
expect(Photo.search).to include crop_photo
end
it "assigns crop" do
expect(assigns(:crop)).to eq crop
end
it { expect(assigns(:photos).size).to eq 1 }
it { expect(assigns(:photos).first.crops).to include crop.id }
it { expect(assigns(:photos).first.id).to eq crop_photo.id }
end
end
end
describe "GET new" do
let(:tomato) { FactoryBot.create(:tomato) }
let(:tomato) { FactoryBot.create(:tomato) }
let(:planting) { FactoryBot.create(:planting, crop: tomato, owner: member) }
let(:garden) { FactoryBot.create(:garden, owner: member) }
let(:harvest) { FactoryBot.create(:harvest, owner: member) }

View File

@@ -2,7 +2,7 @@
require 'rails_helper'
describe PlantingsController do
describe PlantingsController, :search do
login_member
def valid_attributes
@@ -12,32 +12,38 @@ describe PlantingsController do
}
end
describe "GET index" do
describe "GET index", :search do
let!(:member1) { FactoryBot.create(:member) }
let!(:member2) { FactoryBot.create(:member) }
let!(:tomato) { FactoryBot.create(:tomato) }
let!(:maize) { FactoryBot.create(:maize) }
let!(:planting1) { FactoryBot.create :planting, crop: tomato, owner: member1, created_at: 1.day.ago }
let!(:planting2) { FactoryBot.create :planting, crop: maize, owner: member2, created_at: 5.days.ago }
before do
Planting.reindex
end
describe "assigns all plantings as @plantings" do
before { get :index }
it { expect(assigns(:plantings)).to match [planting1, planting2] }
it { expect(assigns(:plantings).size).to eq 2 }
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: member1.slug } }
it { expect(assigns(:owner)).to eq member1 }
it { expect(assigns(:plantings)).to eq [planting1] }
it { expect(assigns(:plantings).size).to eq 1 }
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)).to eq [planting2] }
it { expect(assigns(:plantings).first['slug']).to eq planting2.slug }
end
end
@@ -119,4 +125,19 @@ describe PlantingsController do
it { expect(assigns(:planting).owner).to eq subject.current_member }
end
end
describe 'GET :edit' do
let(:my_planting) { FactoryBot.create :planting, owner: member }
let(:not_my_planting) { FactoryBot.create :planting }
context 'my planting' do
before { get :edit, params: { slug: my_planting } }
it { expect(assigns(:planting)).to eq my_planting }
end
context 'not my planting' do
before { get :edit, params: { slug: not_my_planting } }
it { expect(response).to redirect_to(root_path) }
end
end
end

View File

@@ -2,14 +2,15 @@
require 'rails_helper'
describe SeedsController do
describe SeedsController, :search do
let(:owner) { FactoryBot.create(:member) }
describe "GET index" do
let(:owner) { FactoryBot.create(:member) }
describe "picks up owner from params" do
before { get :index, params: { member_slug: owner.slug } }
before do
Seed.reindex
get :index, params: { member_slug: owner.slug }
end
it { expect(assigns(:owner)).to eq(owner) }
end
@@ -25,9 +26,12 @@ describe SeedsController do
end
context 'with parent planting' do
let(:planting) { FactoryBot.create :planting, owner: owner }
let!(:planting) { FactoryBot.create :planting, owner: owner }
before { get :new, params: { planting_id: planting.to_param } }
before do
Seed.reindex
get :new, params: { planting_slug: planting.to_param }
end
it { expect(assigns(:planting)).to eq(planting) }
end

View File

@@ -53,6 +53,11 @@ FactoryBot.define do
name { "eggplant" }
end
factory :crop_with_photo do
name { 'marshmallow' }
photos { FactoryBot.create_list :photo, 1 }
end
# This should have a name that is alphabetically earlier than :uppercase
# crop to ensure that the ordering tests work.
factory :lowercasecrop do
@@ -81,5 +86,11 @@ FactoryBot.define do
approval_status { "rejected" }
reason_for_rejection { "Totally fake" }
end
trait :reindex do
after(:create) do |crop, _evaluator|
crop.reindex(refresh: true)
end
end
end
end

View File

@@ -27,4 +27,10 @@ FactoryBot.define do
trait :no_description do
description { "" }
end
trait :reindex do
after(:create) do |harvest, _evaluator|
harvest.reindex(refresh: true)
end
end
end

View File

@@ -18,5 +18,11 @@ FactoryBot.define do
license_name { "All rights reserved" }
license_url { nil }
end
trait :reindex do
after(:create) do |photo, _evaluator|
photo.reindex(refresh: true)
end
end
end
end

View File

@@ -61,5 +61,18 @@ FactoryBot.define do
crop
end
end
trait :with_photo do
after(:create) do |planting, _evaluator|
planting.photos << FactoryBot.create(:photo, owner_id: planting.owner_id)
planting.save
end
end
trait :reindex do
after(:create) do |planting, _evaluator|
planting.reindex(refresh: true)
end
end
end
end

View File

@@ -31,5 +31,11 @@ FactoryBot.define do
factory :untradable_seed do
tradable_to { "nowhere" }
end
trait :reindex do
after(:create) do |seed, _evaluator|
seed.reindex(refresh: true)
end
end
end
end

View File

@@ -2,14 +2,18 @@
require 'rails_helper'
describe "browse crops" do
let!(:tomato) { FactoryBot.create :tomato }
let!(:maize) { FactoryBot.create :maize }
let!(:pending_crop) { FactoryBot.create :crop_request }
let!(:rejected_crop) { FactoryBot.create :rejected_crop }
describe "browse crops", :search do
let!(:tomato) { FactoryBot.create :tomato, :reindex }
let!(:maize) { FactoryBot.create :maize, :reindex }
let!(:pending_crop) { FactoryBot.create :crop_request, :reindex }
let!(:rejected_crop) { FactoryBot.create :rejected_crop, :reindex }
shared_examples 'shows crops' do
before { visit crops_path }
before do
Crop.reindex
visit crops_path
end
it "has a form for sorting by" do
expect(page).to have_css "select#sort"
end

View File

@@ -2,15 +2,17 @@
require 'rails_helper'
describe "crop detail page", js: true do
describe "crop detail page", :js, :search do
subject { page }
let!(:owner_member) { FactoryBot.create :member }
let!(:crop) { FactoryBot.create :crop }
let!(:crop) { FactoryBot.create :crop, :reindex }
let(:plant_part) { FactoryBot.create :plant_part, name: 'fruit' }
let!(:harvest) { FactoryBot.create :harvest, crop: crop, owner: owner_member, plant_part: plant_part }
let!(:planting) { FactoryBot.create :planting, crop: crop, owner: owner_member }
let!(:harvest) { FactoryBot.create :harvest, crop: crop, owner: owner_member }
let!(:seed) { FactoryBot.create :seed, crop: crop, owner: owner_member }
let!(:photo1) { FactoryBot.create(:photo, owner: owner_member) }
@@ -27,7 +29,11 @@ describe "crop detail page", js: true do
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

View File

@@ -115,7 +115,7 @@ describe "Planting a crop", js: true do
describe "Making a planting inactive from garden show" do
it do
visit garden_path(garden)
click_link(class: 'planting-menu')
click_link(class: 'planting-menu') # quick menu
click_link "Mark as finished"
find(".datepicker-days td.day", text: "21").click
expect(page).to have_content 'Finished'

View File

@@ -2,17 +2,21 @@
require 'rails_helper'
describe "browse harvests" do
describe "browse harvests", :search do
subject { page }
let!(:harvest) { create :harvest, owner: member }
context 'signed in' do
include_context 'signed in member'
describe 'blank optional fields' do
let!(:harvest) { create :harvest, :no_description }
before { visit harvests_path }
describe 'blank optional fields' do
let!(:harvest) { create :harvest, :no_description, :reindex }
before do
Harvest.reindex
visit harvests_path
end
it 'read more' do
expect(subject).not_to have_link "Read more"
@@ -20,9 +24,10 @@ describe "browse harvests" do
end
describe "filled in optional fields" do
let!(:harvest) { create :harvest, :long_description }
let!(:harvest) { create :harvest, :long_description, :reindex }
before do
Harvest.reindex
visit harvests_path
end

View File

@@ -3,7 +3,7 @@
require 'rails_helper'
require 'custom_matchers'
describe "Harvesting a crop", :js do
describe "Harvesting a crop", :js, :search do
context 'signed in' do
include_context 'signed in member'
let!(:maize) { create :maize }
@@ -40,12 +40,15 @@ describe "Harvesting a crop", :js do
expect(page).to have_content "harvest was successfully created."
end
it "Clicking link to owner's profile" do
visit member_harvests_path(member)
within '.login-name' do
click_link member.login_name
describe 'member harvests' do
before { visit member_harvests_path(member) }
it { expect(page).to have_text "#{member.login_name}'s harvests" }
it "Clicking link to owner's profile" do
within '.login-name' do
click_link member.login_name
end
expect(current_path).to eq member_path(member)
end
expect(current_path).to eq member_path member
end
describe "Harvesting from crop page" do

View File

@@ -2,7 +2,7 @@
require 'rails_helper'
describe "home page" do
describe "home page", :search do
subject { page }
let(:member) { FactoryBot.create :member }
@@ -14,19 +14,24 @@ describe "home page" do
let(:seed) { FactoryBot.create :tradable_seed, owner: member, crop: crop }
let(:harvest) { FactoryBot.create :harvest, owner: member, crop: crop }
let!(:tradable_seed) { FactoryBot.create :tradable_seed, finished: false }
let!(:finished_seed) { FactoryBot.create :tradable_seed, finished: true }
let!(:untradable_seed) { FactoryBot.create :untradable_seed }
let!(:tradable_seed) { FactoryBot.create :tradable_seed, :reindex, finished: false }
let!(:finished_seed) { FactoryBot.create :tradable_seed, :reindex, finished: true }
let!(:untradable_seed) { FactoryBot.create :untradable_seed, :reindex }
before do
# Add photos, so they can appear on home page
planting.photos << photo
seed.photos << photo
harvest.photos << photo
Crop.reindex
end
before { visit root_path }
Crop.reindex
Planting.reindex
Seed.reindex
Harvest.reindex
Photo.reindex
visit root_path
end
shared_examples 'shows seeds' do
it "show tradeable seed" do
@@ -48,6 +53,7 @@ describe "home page" do
it { expect(subject).to have_link href: planting_path(planting) }
end
end
shared_examples 'show harvests' do
describe 'shows harvests section' do
it { expect(subject).to have_text 'Recently Harvested' }

View File

@@ -2,14 +2,21 @@
require 'rails_helper'
describe 'Likeable', js: true do
describe 'Likeable', :js, search: true do
let(:another_member) { FactoryBot.create(:london_member) }
let!(:post) { FactoryBot.create(:post, author: member) }
let!(:photo) { FactoryBot.create(:photo, owner: member) }
let!(:post) { FactoryBot.create(:post, :reindex, author: member) }
let!(:photo) { FactoryBot.create(:photo, :reindex, owner: member) }
before do
Photo.reindex
end
include_context 'signed in member'
describe 'photos' do
let(:like_count_class) { "#photo-#{photo.id} .like-count" }
def like_count_class
"#photo-#{photo.id} .like-count"
end
shared_examples 'photo can be liked' do
it 'can be liked' do

View File

@@ -3,13 +3,15 @@
require "rails_helper"
require 'custom_matchers'
describe "Planting a crop", :js do
describe "Planting a crop", :js, :search do
let!(:maize) { FactoryBot.create :maize }
let(:garden) { FactoryBot.create :garden, owner: member, name: 'Orchard' }
let!(:planting) do
FactoryBot.create :planting, garden: garden, owner: member, planted_at: Date.parse("2013-03-10")
end
before { Planting.reindex }
context 'signed in' do
include_context 'signed in member'
before { visit new_planting_path }
@@ -222,6 +224,9 @@ describe "Planting a crop", :js do
expect(page).to have_content "Finished"
expect(page).to have_content "Aug 2014"
# ensure we've indexed in elastic search
planting.reindex(refresh: true)
# shouldn't be on the page
visit plantings_path
expect(page).not_to have_content "maize"

View File

@@ -4,11 +4,13 @@ require 'rails_helper'
describe 'Crops RSS feed' do
it 'The index feed exists' do
Crop.reindex
visit crops_path(format: 'rss')
# expect(page.status_code).to equal 200
end
it 'The index title is what we expect' do
Crop.reindex
visit crops_path(format: 'rss')
expect(page).to have_content "Recently added crops (#{ENV['GROWSTUFF_SITE_NAME']})"
end

View File

@@ -9,6 +9,7 @@ describe 'Plantings RSS feed' do
end
it 'The index title is what we expect' do
Planting.reindex
visit plantings_path(format: 'rss')
expect(page).to have_content "Recent plantings from "\
"#{@owner || 'all members'} (#{ENV['GROWSTUFF_SITE_NAME']})"

View File

@@ -3,7 +3,7 @@
require 'rails_helper'
require 'custom_matchers'
describe "Seeds", :js do
describe "Seeds", :js, :search do
context 'signed in' do
include_context 'signed in member'
let!(:maize) { create :maize }

View File

@@ -3,5 +3,12 @@
require 'rails_helper'
RSpec.describe CropCompanion, type: :model do
pending "add some examples to (or delete) #{__FILE__}"
it 'has a crop' do
cc = CropCompanion.new
cc.crop_a = FactoryBot.create :tomato
cc.crop_b = FactoryBot.create :maize
cc.save!
expect(cc.crop_a.name).to eq 'tomato'
end
end

View File

@@ -5,6 +5,7 @@ require 'rails_helper'
describe 'like' do
let(:member) { FactoryBot.create(:member) }
let(:post) { FactoryBot.create(:post) }
let(:photo) { FactoryBot.create :photo }
context 'existing like' do
before do
@@ -61,4 +62,14 @@ describe 'like' do
member.destroy
expect(Like.all).not_to include like
end
it 'liked_by_members_names' do
expect(post.liked_by_members_names).to eq []
Like.create(member: member, likeable: post)
expect(post.liked_by_members_names).to eq [member.login_name]
expect(photo.liked_by_members_names).to eq []
Like.create(member: member, likeable: photo)
expect(photo.liked_by_members_names).to eq [member.login_name]
end
end

View File

@@ -3,8 +3,7 @@
require 'rails_helper'
describe Photo do
let(:photo) { FactoryBot.create(:photo, owner: member) }
let(:old_photo) { FactoryBot.create(:photo, owner: member, created_at: 1.year.ago, date_taken: 2.years.ago) }
let(:photo) { FactoryBot.create(:photo, :reindex, owner: member) }
let(:member) { FactoryBot.create(:member) }
it_behaves_like "it is likeable"
@@ -20,13 +19,14 @@ describe Photo do
describe 'to a planting' do
before { planting.photos << photo }
it { expect(planting.photos.size).to eq 1 }
it { expect(planting.photos.count).to eq 1 }
it { expect(planting.photos.first).to eq photo }
# there's only one photo, so that's the default
it { expect(planting.default_photo).to eq photo }
it { expect(planting.crop.default_photo).to eq photo }
describe 'with a second older photo' do
let(:old_photo) { FactoryBot.create(:photo, owner: member, created_at: 1.year.ago, date_taken: 2.years.ago) }
# Add an old photo
before { planting.photos << old_photo }
it { expect(planting.default_photo).to eq photo }
@@ -40,28 +40,45 @@ describe Photo do
end
end
it 'to a harvest' do
harvest.photos << photo
expect(harvest.photos.size).to eq 1
expect(harvest.photos.first).to eq photo
describe 'to a harvest' do
let(:crop) { harvest.crop }
before { harvest.photos << photo }
it { expect(harvest.photos).to eq [photo] }
it { expect(harvest.photo_associations.count).to eq 1 }
# Check the relationship from crop
it { expect(crop.photo_associations.count).to eq 1 }
it { expect(crop.photos.count).to eq 1 }
it { expect(crop.photos).to eq [photo] }
# Check the relationship from the photo
it { expect(photo.photo_associations.count).to eq 1 }
it { expect(photo.photo_associations.map(&:crop)).to eq [ crop ] }
it { expect(photo.crops.count).to eq 1 }
it { expect(photo.crops).to eq [crop] }
end
it 'to a seed' do
seed.photos << photo
expect(seed.photos.size).to eq 1
expect(seed.photos.first).to eq photo
expect(seed.photos).to eq [photo]
expect(photo.crops).to eq [seed.crop]
end
it 'to a planting' do
planting.photos << photo
expect(planting.photos).to eq [photo]
expect(photo.crops).to eq [planting.crop]
end
it 'to a garden' do
garden.photos << photo
expect(garden.photos.size).to eq 1
expect(garden.photos.first).to eq photo
expect(garden.photos).to eq [photo]
end
it 'to a post' do
post.photos << photo
expect(post.photos.size).to eq 1
expect(post.photos.first).to eq photo
expect(post.photos).to eq [photo]
end
end
@@ -69,19 +86,19 @@ describe Photo do
it 'from a planting' do
planting.photos << photo
photo.destroy
expect(planting.photos.size).to eq 0
expect(planting.photos.count).to eq 0
end
it 'from a harvest' do
harvest.photos << photo
photo.destroy
expect(harvest.photos.size).to eq 0
expect(harvest.photos.count).to eq 0
end
it 'from a garden' do
garden.photos << photo
photo.destroy
expect(garden.photos.size).to eq 0
expect(garden.photos.count).to eq 0
end
it "automatically if unused" do
@@ -117,23 +134,23 @@ describe Photo do
planting.photos << photo
harvest.photos << photo
garden.photos << photo
expect(photo.plantings.size).to eq 1
expect(photo.harvests.size).to eq 1
expect(photo.gardens.size).to eq 1
expect(photo.plantings.count).to eq 1
expect(photo.harvests.count).to eq 1
expect(photo.gardens.count).to eq 1
planting.destroy # photo is still used by harvest and garden
photo.reload
expect(photo.plantings.size).to eq 0
expect(photo.harvests.size).to eq 1
expect(photo.plantings.count).to eq 0
expect(photo.harvests.count).to eq 1
harvest.destroy
garden.destroy # photo is now no longer used by anything
photo.reload
expect(photo.plantings.size).to eq 0
expect(photo.harvests.size).to eq 0
expect(photo.gardens.size).to eq 0
expect(photo.plantings.count).to eq 0
expect(photo.harvests.count).to eq 0
expect(photo.gardens.count).to eq 0
photo.destroy_if_unused
expect(-> { photo.reload }).to raise_error ActiveRecord::RecordNotFound
end
@@ -163,16 +180,16 @@ describe Photo do
expect(Photo.joins(:owner).all).not_to include(photo)
end
describe 'scopes' do
let(:harvest_crop) { FactoryBot.create :crop }
describe 'assocations' do
let(:harvest_crop) { FactoryBot.create :crop, name: 'harvest_crop' }
let!(:harvest) { FactoryBot.create :harvest, owner: member, crop: harvest_crop }
let!(:harvest_photo) { FactoryBot.create :photo, owner: member }
let(:planting_crop) { FactoryBot.create :crop }
let(:planting_crop) { FactoryBot.create :crop, name: 'planting_crop' }
let!(:planting) { FactoryBot.create :planting, owner: member, crop: planting_crop }
let!(:planting_photo) { FactoryBot.create :photo, owner: member }
let(:seed_crop) { FactoryBot.create :crop }
let(:seed_crop) { FactoryBot.create :crop, name: 'seed_crop' }
let!(:seed) { FactoryBot.create :seed, owner: member, crop: seed_crop }
let!(:seed_photo) { FactoryBot.create :photo, owner: member }
@@ -180,14 +197,70 @@ describe Photo do
harvest.photos << harvest_photo
planting.photos << planting_photo
seed.photos << seed_photo
# harvest_photo.reload
# harvest.reload
# # harvest.reindex
# planting_photo.reload
# planting.reload
# # planting.reindex
# seed_photo.reload
# seed.reload
# seed.reindex
end
it { expect(Photo.by_model(Harvest)).to eq([harvest_photo]) }
it { expect(Photo.by_model(Planting)).to eq([planting_photo]) }
it { expect(Photo.by_model(Seed)).to eq([seed_photo]) }
describe 'relationships' do
it { expect(seed_photo.crops).to eq [seed_crop] }
it { expect(seed_crop.photos).to eq [seed_photo] }
it { expect(Photo.by_crop(harvest_crop)).to eq([harvest_photo]) }
it { expect(Photo.by_crop(planting_crop)).to eq([planting_photo]) }
it { expect(Photo.by_crop(seed_crop)).to eq([seed_photo]) }
it { expect(harvest_photo.crops).to eq [harvest_crop] }
it { expect(harvest_crop.photos).to eq [harvest_photo] }
it { expect(planting_photo.crops).to eq [planting_crop] }
it { expect(planting_crop.photos).to eq [planting_photo] }
end
describe 'scopes' do
it { expect(Photo.by_model(Harvest)).to eq([harvest_photo]) }
it { expect(Photo.by_model(Planting)).to eq([planting_photo]) }
it { expect(Photo.by_model(Seed)).to eq([seed_photo]) }
it { expect(Photo.by_crop(harvest_crop)).to eq([harvest_photo]) }
it { expect(Photo.by_crop(planting_crop)).to eq([planting_photo]) }
it { expect(Photo.by_crop(seed_crop)).to eq([seed_photo]) }
end
end
describe 'Elastic search indexing', search: true do
let!(:planting) { FactoryBot.create(:planting, :reindex, owner: photo.owner) }
let!(:crop) { FactoryBot.create :crop, :reindex }
before do
planting.photos << photo
Photo.reindex
Photo.searchkick_index.refresh
end
describe "finds all photos in search index" do
it "finds just one" do
expect(Photo.search.count).to eq 1
end
it "finds the matching photo" do
expect(Photo.search).to include photo
end
it "retrieves crops from ES" do
expect(Photo.search(load: false).first.crops).to eq [planting.crop.id]
end
end
it "finds photos by owner in search index" do
expect(Photo.search(where: { owner_id: planting.owner_id })).to include photo
end
it "finds photos by crop in search index" do
expect(Photo.search(where: { crops: planting.crop.id })).to include photo
end
end
end

View File

@@ -179,8 +179,8 @@ describe Planting do
before do
FactoryBot.create(:harvest,
planting: planting,
crop: planting.crop,
planting: planting,
crop: planting.crop,
harvested_at: 10.days.ago)
planting.update_harvest_days!
planting.crop.update_harvest_medians
@@ -232,8 +232,11 @@ describe Planting do
before do
# Near by planting with harvests
nearby_garden = FactoryBot.create :garden, location: 'Greenwich, UK'
nearby_planting = FactoryBot.create :planting, crop: crop,
garden: nearby_garden, owner: nearby_garden.owner, planted_at: '1 January 2000'
nearby_planting = FactoryBot.create(:planting,
crop: crop,
garden: nearby_garden,
owner: nearby_garden.owner,
planted_at: '1 January 2000')
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
harvested_at: '1 May 2019'
FactoryBot.create :harvest, planting: nearby_planting, crop: crop,
@@ -354,9 +357,11 @@ describe Planting do
end
it 'all valid planted_from values should work' do
['seed', 'seedling', 'cutting', 'root division',
'runner', 'bare root plant', 'advanced plant',
'graft', 'layering', 'bulb', 'root/tuber', nil, ''].each do |p|
[
'seed', 'seedling', 'cutting', 'root division',
'runner', 'bare root plant', 'advanced plant',
'graft', 'layering', 'bulb', 'root/tuber', nil, ''
].each do |p|
@planting = FactoryBot.build(:planting, planted_from: p)
@planting.should be_valid
end
@@ -407,16 +412,10 @@ describe Planting do
before do
# plantings have members created implicitly for them
# each member is different, hence these are all interesting
@planting1 = FactoryBot.create(:planting, planted_at: 5.days.ago)
@planting2 = FactoryBot.create(:planting, planted_at: 4.days.ago)
@planting3 = FactoryBot.create(:planting, planted_at: 3.days.ago)
@planting4 = FactoryBot.create(:planting, planted_at: 2.days.ago)
# plantings need photos to be interesting
[@planting1, @planting2, @planting3, @planting4].each do |p|
p.photos << FactoryBot.create(:photo, owner_id: p.owner_id)
p.save
end
@planting1 = FactoryBot.create(:planting, :with_photo, planted_at: 5.days.ago)
@planting2 = FactoryBot.create(:planting, :with_photo, planted_at: 4.days.ago)
@planting3 = FactoryBot.create(:planting, :with_photo, planted_at: 3.days.ago)
@planting4 = FactoryBot.create(:planting, :with_photo, planted_at: 2.days.ago)
end
it { expect(Planting.interesting).to eq([@planting4, @planting3, @planting2, @planting1]) }
@@ -445,8 +444,8 @@ describe Planting do
# this one is newer, and has the same owner, through the garden
@planting2 = FactoryBot.create(:planting,
created_at: 1.minute.ago,
garden: @planting1.garden,
owner: @planting1.owner)
garden: @planting1.garden,
owner: @planting1.owner)
@planting2.photos << FactoryBot.create(:photo, owner: @planting2.owner)
@planting2.save
@@ -541,4 +540,16 @@ describe Planting do
it { expect(member.plantings.active).to include(planting) }
it { expect(member.plantings.active).not_to include(finished_planting) }
end
describe 'homepage', :search do
let!(:interesting_planting) { FactoryBot.create :planting, :reindex, :with_photo }
let!(:finished_interesting_planting) { FactoryBot.create :finished_planting, :reindex, :with_photo }
let!(:planting) { FactoryBot.create :planting, :reindex }
before { Planting.reindex }
subject { Planting.homepage_records(100) }
it { expect(subject.count).to eq 2 }
it { expect(subject.map(&:id)).to eq([interesting_planting.id.to_s, finished_interesting_planting.id.to_s]) }
end
end

View File

@@ -63,23 +63,23 @@ describe Seed do
@seed.should_not be_valid
end
it 'tradable? gives the right answers' do
it 'tradable gives the right answers' do
@seed = FactoryBot.create(:seed, tradable_to: 'nowhere')
@seed.tradable?.should eq false
@seed.tradable.should eq false
@seed = FactoryBot.create(:seed, tradable_to: 'locally')
@seed.tradable?.should eq true
@seed.tradable.should eq true
@seed = FactoryBot.create(:seed, tradable_to: 'nationally')
@seed.tradable?.should eq true
@seed.tradable.should eq true
@seed = FactoryBot.create(:seed, tradable_to: 'internationally')
@seed.tradable?.should eq true
@seed.tradable.should eq true
end
it 'recognises a tradable seed' do
FactoryBot.create(:tradable_seed).tradable?.should == true
FactoryBot.create(:tradable_seed).tradable.should == true
end
it 'recognises an untradable seed' do
FactoryBot.create(:untradable_seed).tradable?.should == false
FactoryBot.create(:untradable_seed).tradable.should == false
end
it 'scopes correctly' do
@@ -197,4 +197,16 @@ describe Seed do
end
end
end
describe 'homepage', :search do
let!(:tradable_seed) { FactoryBot.create :tradable_seed, :reindex, finished: false }
let!(:finished_seed) { FactoryBot.create :tradable_seed, :reindex, finished: true }
let!(:untradable_seed) { FactoryBot.create :untradable_seed, :reindex }
before { Seed.reindex }
subject { Seed.homepage_records(100) }
it { expect(subject.count).to eq 1 }
it { expect(subject.first.id).to eq tradable_seed.id.to_s }
end
end

View File

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

View File

@@ -5,9 +5,8 @@ require 'rails_helper'
describe "Harvests" do
describe "GET /harvests" 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 harvests_path
response.status.should be(200)
expect(response.status).to be 200
end
end
end

View File

@@ -20,6 +20,7 @@ require 'simplecov'
require 'percy'
SimpleCov.start
RSpec.configure do |config|
# rspec-expectations config goes here. You can use an alternate
# assertion/expectation library such as wrong or the stdlib/minitest
@@ -36,9 +37,17 @@ RSpec.configure do |config|
expectations.include_chain_clauses_in_custom_matcher_descriptions = true
end
config.before(:suite) do
def index_everything
# reindex models
Crop.reindex
Harvest.reindex
Photo.reindex
Planting.reindex
Seed.reindex
end
config.before(:suite) do
index_everything
# and disable callbacks
Searchkick.disable_callbacks
@@ -46,7 +55,9 @@ RSpec.configure do |config|
config.around(:each, search: true) do |example|
Searchkick.callbacks(true) do
index_everything
example.run
index_everything
end
end

View File

@@ -10,8 +10,8 @@ describe "forums/show" do
it "renders attributes" do
render
rendered.should have_content "Everything about permaculture"
rendered.should have_content @forum.owner.to_s
expect(rendered).to have_content "Everything about permaculture"
expect(rendered).to have_content @forum.owner.to_s
end
it "parses markdown description into html" do
@@ -26,14 +26,14 @@ describe "forums/show" do
it 'has no posts' do
render
rendered.should have_content "No posts yet."
expect(rendered).to have_content "No posts yet."
end
it 'shows posts' do
@post = FactoryBot.create(:post, forum: @forum)
render
assert_select "table"
rendered.should have_content @post.subject
rendered.should have_content @post.author.to_s
expect(rendered).to have_content @post.subject
expect(rendered).to have_content @post.author.to_s
end
end

View File

@@ -2,43 +2,38 @@
require 'rails_helper'
describe 'harvests/index.rss.haml' do
describe 'harvests/index.rss.haml', :search do
before do
controller.stub(:current_user) { nil }
@member = FactoryBot.create(:member)
@tomato = FactoryBot.create(:tomato)
@maize = FactoryBot.create(:maize)
@pp = FactoryBot.create(:plant_part)
page = 1
per_page = 2
total_entries = 2
harvests = WillPaginate::Collection.create(page, per_page, total_entries) do |pager|
pager.replace([
FactoryBot.create(:harvest,
crop: @tomato,
owner: @member),
FactoryBot.create(:harvest,
crop: @maize,
plant_part: @pp,
owner: @member,
quantity: 2)
])
@harvest1 = FactoryBot.create :harvest, crop: @tomato
@harvest2 = FactoryBot.create :harvest, crop: @tomato
@harvest3 = FactoryBot.create :harvest, crop: @tomato
Harvest.searchkick_index.refresh
assign(:harvests, Harvest.search(load: false))
end
context 'all harvests' do
before { render }
it 'shows RSS feed title' do
expect(rendered).to have_content "Recent harvests from all members"
end
it 'shows formatted content of harvest posts' do
expect(rendered).to have_content "<p>Quantity: "
end
assign(:harvests, harvests)
render
end
it 'shows RSS feed title' do
rendered.should have_content "Recent harvests from all members"
end
it "displays crop's name in title" do
assign(:crop, @tomato)
render
expect(rendered).to have_content @tomato.name
end
it 'shows formatted content of harvest posts' do
expect(rendered).to have_content "<p>Quantity: "
context 'for one crop' do
before do
assign(:crop, @tomato)
render
end
it "displays crop's name in title" do
expect(rendered).to have_content @tomato.name
end
end
end

View File

@@ -2,10 +2,13 @@
require 'rails_helper'
describe 'home/_seeds.html.haml', type: "view" do
describe 'home/_seeds.html.haml', type: "view", search: true do
let!(:seed) { FactoryBot.create(:tradable_seed, owner: owner) }
let(:owner) { FactoryBot.create(:london_member) }
before { render }
before do
Seed.searchkick_index.refresh
render
end
it 'has a heading' do
assert_select 'h2', 'Seeds available to trade'

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