From a7d56d4f9261a0964d1d055e7945a4059da69976 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 20 Jul 2014 17:27:17 +1000 Subject: [PATCH 001/132] Added stub code for sending regular (eg. weekly) emails --- Gemfile | 1 + Gemfile.lock | 9 +++++++++ app/controllers/members_controller.rb | 4 ++++ app/mailers/notifier.rb | 7 +++++++ app/models/ability.rb | 1 + app/views/members/send_email.html.haml | 2 ++ app/views/notifier/regular_email.html.haml | 22 ++++++++++++++++++++++ config/routes.rb | 2 +- spec/features/regular_email_spec.rb | 20 ++++++++++++++++++++ 9 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 app/views/members/send_email.html.haml create mode 100644 app/views/notifier/regular_email.html.haml create mode 100644 spec/features/regular_email_spec.rb diff --git a/Gemfile b/Gemfile index b4ff013a0..23dbbb978 100644 --- a/Gemfile +++ b/Gemfile @@ -124,4 +124,5 @@ group :development, :test do gem 'webrat' # provides HTML matchers for view tests gem 'factory_girl_rails', '~> 4.0' # for creating test data gem 'coveralls', require: false # coverage analysis + gem 'capybara' end diff --git a/Gemfile.lock b/Gemfile.lock index e2bd35b31..6095c5556 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -73,6 +73,12 @@ GEM railties (>= 3.0) builder (3.0.4) cancan (1.6.10) + capybara (2.3.0) + mime-types (>= 1.16) + nokogiri (>= 1.3.3) + rack (>= 1.0.0) + rack-test (>= 0.5.4) + xpath (~> 2.0) chunky_png (1.3.1) coderay (1.1.0) coffee-rails (3.2.2) @@ -277,6 +283,8 @@ GEM rack (>= 1.0) rack-test (>= 0.5.3) will_paginate (3.0.5) + xpath (2.0.0) + nokogiri (~> 1.3) PLATFORMS ruby @@ -290,6 +298,7 @@ DEPENDENCIES bootstrap-datepicker-rails bundler (>= 1.1.5) cancan + capybara coffee-rails (~> 3.2.1) compass-rails (~> 1.0.3) coveralls diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index ab3de0001..80c153ddc 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -32,4 +32,8 @@ class MembersController < ApplicationController end end + def send_email + Notifier.regular_email(current_member).deliver! + end + end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 579b2a8ad..7850d3623 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -9,4 +9,11 @@ class Notifier < ActionMailer::Base mail(:to => @notification.recipient.email, :subject => @notification.subject) end + + def regular_email(member) + @member = member + mail(:to => @member.email, + :subject => "This is your regular contact email") + end + end diff --git a/app/models/ability.rb b/app/models/ability.rb index e33f4a022..2c3764d9f 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -23,6 +23,7 @@ class Ability # managing your own user settings can :update, Member, :id => member.id + can :send_email, Member # can read/delete notifications that were sent to them can :read, Notification, :recipient_id => member.id diff --git a/app/views/members/send_email.html.haml b/app/views/members/send_email.html.haml new file mode 100644 index 000000000..e543ae1a2 --- /dev/null +++ b/app/views/members/send_email.html.haml @@ -0,0 +1,2 @@ +Sending email! + diff --git a/app/views/notifier/regular_email.html.haml b/app/views/notifier/regular_email.html.haml new file mode 100644 index 000000000..c32821a68 --- /dev/null +++ b/app/views/notifier/regular_email.html.haml @@ -0,0 +1,22 @@ +- site_name = ENV['GROWSTUFF_SITE_NAME'] +%p Hello #{@member.login_name}, + +%p + This is your regular weekly email. + +%p Stuff to include: + +%ul + %li Recent plantings (yours, people near you) + %li Recent harvests (yours, people near you) + %li Seeds available near you + %li New members near you + %li New discussion posts + +%p + = link_to "Turn off these notifications", edit_member_registration_url + +%p + The #{site_name} team. + %br/ + =link_to root_url, root_url diff --git a/config/routes.rb b/config/routes.rb index 9f066fa47..eb147a877 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ Growstuff::Application.routes.draw do resources :plant_parts - + get '/members/send_email' => 'members#send_email', :as => 'send_email' devise_for :members, :controllers => { :registrations => "registrations" } resources :members diff --git a/spec/features/regular_email_spec.rb b/spec/features/regular_email_spec.rb new file mode 100644 index 000000000..033c8b324 --- /dev/null +++ b/spec/features/regular_email_spec.rb @@ -0,0 +1,20 @@ +require 'spec_helper' + +feature "regular email" do + before :each do + @member = FactoryGirl.create(:member) + visit root_path + click_link 'Sign in' + page.should have_content "Sign in" + fill_in 'Login', :with => @member.login_name + fill_in 'Password', :with => @member.password + click_button 'Sign in' + end + + scenario "sends email" do + expect { + # stub for while we're working on this. remove! + visit send_email_path + }.to change { ActionMailer::Base.deliveries.count }.by(1) + end +end From f4c2c2481d90d6c8db706e21bddff7b73b603d76 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 20 Jul 2014 18:45:48 +1000 Subject: [PATCH 002/132] Include plantings/harvests in regular email --- app/mailers/notifier.rb | 4 + app/models/harvest.rb | 35 +++++++++ app/views/notifier/regular_email.html.haml | 38 ++++++++-- spec/models/harvest_spec.rb | 86 ++++++++++++++++++++++ 4 files changed, 155 insertions(+), 8 deletions(-) diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 7850d3623..3cc2556b9 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -12,6 +12,10 @@ class Notifier < ActionMailer::Base def regular_email(member) @member = member + + @plantings = member.plantings.reorder.last(5) + @harvests = member.harvests.reorder.last(5) + mail(:to => @member.email, :subject => "This is your regular contact email") end diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 3afaeb170..c58a53c30 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -1,4 +1,5 @@ class Harvest < ActiveRecord::Base + include ActionView::Helpers::NumberHelper extend FriendlyId friendly_id :harvest_slug, use: :slugged @@ -69,4 +70,38 @@ class Harvest < ActiveRecord::Base "#{owner.login_name}-#{crop}".downcase.gsub(' ', '-') end + # stringify as "beet in Skud's backyard" or similar + def to_s + # 50 individual apples, weighing 3lb + # 2 buckets of apricots, weighing 10kg + string = '' + if self.quantity + string += "#{number_to_human(self.quantity.to_s, :strip_insignificant_zeros => true)} " + if self.unit == 'individual' + string += 'individual ' + else + if self.quantity == 1 + string += "#{self.unit} of " + else + string += "#{self.unit.pluralize} of " + end + end + end + + if self.unit != 'individual' # buckets of apricot*s* + string += "#{self.crop.name.pluralize}" + elsif self.quantity == 1 + string += "#{self.crop.name}" + else + string += "#{self.crop.name.pluralize}" + end + + if self.weight_quantity + string += " weighing #{number_to_human(self.weight_quantity, :strip_insignificant_zeros => true)} #{self.weight_unit}" + end + + return string + + end + end diff --git a/app/views/notifier/regular_email.html.haml b/app/views/notifier/regular_email.html.haml index c32821a68..1e77fc195 100644 --- a/app/views/notifier/regular_email.html.haml +++ b/app/views/notifier/regular_email.html.haml @@ -1,19 +1,41 @@ - site_name = ENV['GROWSTUFF_SITE_NAME'] %p Hello #{@member.login_name}, -%p - This is your regular weekly email. +%h2 What's new in your garden? -%p Stuff to include: +Have you planted anything recently? The most recent plantings you've +told us about are: %ul - %li Recent plantings (yours, people near you) - %li Recent harvests (yours, people near you) - %li Seeds available near you - %li New members near you - %li New discussion posts +- @plantings.each do |p| + %li + = link_to p, planting_url(p) + planted + = distance_of_time_in_words(p.created_at, Time.zone.now) + ago. %p + Planted anything new? + = link_to "Track your plantings here.", new_planting_url + +%h2 Your recent harvests + +According to our records, the last few things you harvested were: + +%ul +- @harvests.each do |h| + %li + = link_to h, harvest_url(h) + harvested + = distance_of_time_in_words(h.created_at, Time.zone.now) + ago. + +%p + Harvested anything else lately? + = link_to "Track your harvests here.", new_harvest_url + +%p + Don't want to get these emails any more? = link_to "Turn off these notifications", edit_member_registration_url %p diff --git a/spec/models/harvest_spec.rb b/spec/models/harvest_spec.rb index a7da223aa..3555e390d 100644 --- a/spec/models/harvest_spec.rb +++ b/spec/models/harvest_spec.rb @@ -125,4 +125,90 @@ describe Harvest do Harvest.all.should eq [@h2, @h1] end end + + context "stringification" do + before :each do + @crop = FactoryGirl.create(:crop, :name => "apricot") + end + + it "apricots" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => nil, + :unit => nil, + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "apricots" + end + + it "1 individual apricot" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 1, + :unit => 'individual', + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "1 individual apricot" + end + + it "10 individual apricots" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 10, + :unit => 'individual', + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "10 individual apricots" + end + + it "1 bushel of apricots" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 1, + :unit => 'bushel', + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "1 bushel of apricots" + end + + it "1.5 bushels of apricots" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 1.5, + :unit => 'bushel', + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "1.5 bushels of apricots" + end + + it "10 bushels of apricots" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 10, + :unit => 'bushel', + :weight_quantity => nil, + :weight_unit => nil + ) + @h.to_s.should eq "10 bushels of apricots" + end + + it "apricots weighing 1.2 kg" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => nil, + :unit => nil, + :weight_quantity => 1.2, + :weight_unit => 'kg' + ) + @h.to_s.should eq "apricots weighing 1.2 kg" + end + + it "10 bushels of apricots weighing 100 kg" do + @h = FactoryGirl.create(:harvest, :crop => @crop, + :quantity => 10, + :unit => 'bushel', + :weight_quantity => 100, + :weight_unit => 'kg') + @h.to_s.should eq "10 bushels of apricots weighing 100 kg" + end + + end end From 7c09188bb8066b4831ce253bc718c36417b9197d Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 20 Jul 2014 18:54:58 +1000 Subject: [PATCH 003/132] Renamed email to planting_reminder --- app/controllers/members_controller.rb | 4 ++-- app/mailers/notifier.rb | 4 ++-- app/models/ability.rb | 2 +- app/views/members/send_email.html.haml | 2 -- app/views/members/send_planting_reminder.html.haml | 8 ++++++++ ...egular_email.html.haml => planting_reminder.html.haml} | 0 config/routes.rb | 2 +- .../{regular_email_spec.rb => planting_reminder_spec.rb} | 4 ++-- 8 files changed, 16 insertions(+), 10 deletions(-) delete mode 100644 app/views/members/send_email.html.haml create mode 100644 app/views/members/send_planting_reminder.html.haml rename app/views/notifier/{regular_email.html.haml => planting_reminder.html.haml} (100%) rename spec/features/{regular_email_spec.rb => planting_reminder_spec.rb} (86%) diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index 80c153ddc..f1e00a37a 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -32,8 +32,8 @@ class MembersController < ApplicationController end end - def send_email - Notifier.regular_email(current_member).deliver! + def send_planting_reminder + Notifier.planting_reminder(current_member).deliver! end end diff --git a/app/mailers/notifier.rb b/app/mailers/notifier.rb index 3cc2556b9..637c3b2fe 100644 --- a/app/mailers/notifier.rb +++ b/app/mailers/notifier.rb @@ -10,14 +10,14 @@ class Notifier < ActionMailer::Base :subject => @notification.subject) end - def regular_email(member) + def planting_reminder(member) @member = member @plantings = member.plantings.reorder.last(5) @harvests = member.harvests.reorder.last(5) mail(:to => @member.email, - :subject => "This is your regular contact email") + :subject => "What have you planted lately?") end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 2c3764d9f..8e4e30fe6 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -23,7 +23,7 @@ class Ability # managing your own user settings can :update, Member, :id => member.id - can :send_email, Member + can :send_planting_reminder, Member # can read/delete notifications that were sent to them can :read, Notification, :recipient_id => member.id diff --git a/app/views/members/send_email.html.haml b/app/views/members/send_email.html.haml deleted file mode 100644 index e543ae1a2..000000000 --- a/app/views/members/send_email.html.haml +++ /dev/null @@ -1,2 +0,0 @@ -Sending email! - diff --git a/app/views/members/send_planting_reminder.html.haml b/app/views/members/send_planting_reminder.html.haml new file mode 100644 index 000000000..e83f4c36d --- /dev/null +++ b/app/views/members/send_planting_reminder.html.haml @@ -0,0 +1,8 @@ +- content_for :title, "Sent planting reminder" + +%p + We just sent you a planting reminder email. + +%p + = link_to "Back to your profile", edit_member_registration_path + diff --git a/app/views/notifier/regular_email.html.haml b/app/views/notifier/planting_reminder.html.haml similarity index 100% rename from app/views/notifier/regular_email.html.haml rename to app/views/notifier/planting_reminder.html.haml diff --git a/config/routes.rb b/config/routes.rb index eb147a877..09de6fe73 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,7 @@ Growstuff::Application.routes.draw do resources :plant_parts - get '/members/send_email' => 'members#send_email', :as => 'send_email' + get '/members/send_planting_reminder' => 'members#send_planting_reminder', :as => 'send_planting_reminder' devise_for :members, :controllers => { :registrations => "registrations" } resources :members diff --git a/spec/features/regular_email_spec.rb b/spec/features/planting_reminder_spec.rb similarity index 86% rename from spec/features/regular_email_spec.rb rename to spec/features/planting_reminder_spec.rb index 033c8b324..763331442 100644 --- a/spec/features/regular_email_spec.rb +++ b/spec/features/planting_reminder_spec.rb @@ -1,6 +1,6 @@ require 'spec_helper' -feature "regular email" do +feature "planting reminder" do before :each do @member = FactoryGirl.create(:member) visit root_path @@ -14,7 +14,7 @@ feature "regular email" do scenario "sends email" do expect { # stub for while we're working on this. remove! - visit send_email_path + visit send_planting_reminder_path }.to change { ActionMailer::Base.deliveries.count }.by(1) end end From 7d54ab347fbc8bf5c367bd0b8f150b0b7077b546 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Tue, 9 Sep 2014 19:55:40 -0400 Subject: [PATCH 004/132] Adding photo functionality to harvests --- app/controllers/photos_controller.rb | 41 ++++++++---- app/models/harvest.rb | 7 ++ app/models/photo.rb | 6 +- app/views/harvests/show.html.haml | 13 ++++ app/views/photos/new.html.haml | 2 +- app/views/plantings/show.html.haml | 2 +- config/initializers/devise.rb | 3 +- db/schema.rb | 7 +- spec/controllers/photos_controller_spec.rb | 74 +++++++++++++++++++--- spec/models/harvest_spec.rb | 28 ++++++++ 10 files changed, 158 insertions(+), 25 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 4962fc54e..8932ebae8 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -29,7 +29,8 @@ class PhotosController < ApplicationController # GET /photos/new.json def new @photo = Photo.new - @planting_id = params[:planting_id] + @type = params[:type] + @id = params[:id] page = params[:page] || 1 @@ -63,17 +64,33 @@ class PhotosController < ApplicationController @photo.owner_id = current_member.id @photo.set_flickr_metadata - if params[:planting_id] - planting = Planting.find_by_id(params[:planting_id]) - if planting - if planting.owner.id == current_member.id - @photo.plantings << planting unless @photo.plantings.include?(planting) - else - flash[:alert] = "You must own both the planting and the photo." - end - else - flash[:alert] = "Couldn't find planting to connect to photo." - end + # several models can have photos. we need to know what model and the id + # for the entry to attach the photo to + valid_models = ["planting", "harvest"] + if params[:type] + if valid_models.include?(params[:type]) + if params[:id] + item = params[:type].camelcase.constantize.find_by_id(params[:id]) + if item + if item.owner.id == current_member.id + # This syntax is weird, so just know that it means this: + # @photo.harvests << item unless @photo.harvests.include?(item) + # but with the correct many-to-many relationship automatically referenced + (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) + else + flash[:alert] = "You must own both the #{params[:type]} and the photo." + end + else + flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." + end + else + flash[:alert] = "Missing id parameter" + end + else + flash[:alert] = "Cannot attach photos to #{params[:type]}" + end + else + flash[:alert] = "Missing type parameter" end respond_to do |format| diff --git a/app/models/harvest.rb b/app/models/harvest.rb index a7851fdc8..8cc54e043 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -9,6 +9,9 @@ class Harvest < ActiveRecord::Base belongs_to :owner, :class_name => 'Member' belongs_to :plant_part + has_and_belongs_to_many :photos + before_destroy {|harvest| harvest.photos.clear} + default_scope order('created_at DESC') validates :crop, :presence => {:message => "must be present and exist in our database"} @@ -72,4 +75,8 @@ class Harvest < ActiveRecord::Base "#{owner.login_name}-#{crop}".downcase.gsub(' ', '-') end + def default_photo + return photos.first + end + end diff --git a/app/models/photo.rb b/app/models/photo.rb index e65617c87..aa5a624d6 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -4,7 +4,11 @@ class Photo < ActiveRecord::Base belongs_to :owner, :class_name => 'Member' has_and_belongs_to_many :plantings - before_destroy {|photo| photo.plantings.clear} + has_and_belongs_to_many :harvests + before_destroy do |photo| + photo.plantings.clear + photo.harvests.clear + end default_scope order("created_at desc") diff --git a/app/views/harvests/show.html.haml b/app/views/harvests/show.html.haml index 95897bd42..fe0690f1e 100644 --- a/app/views/harvests/show.html.haml +++ b/app/views/harvests/show.html.haml @@ -34,3 +34,16 @@ :growstuff_markdown #{ @harvest.description != "" ? @harvest.description : "No description given." } + +- if @harvest.photos.count > 0 or (can? :edit, @harvest and can? :create, Photo) + %h2 Pictures + + %ul.thumbnails + - @harvest.photos.each do |p| + %li.span2.six-across + = render :partial => 'photos/thumbnail', :locals => { :photo => p } + - if can? :create, Photo and can? :edit, @harvest + %li.span2 + .thumbnail(style='height: 220px') + %p{:style => 'text-align: center; padding-top: 50px'} + = link_to "Add photo", new_photo_path(:type => "harvest", :id => @harvest.id), :class => 'btn btn-primary' diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index 32ea66915..965ff28e4 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -25,7 +25,7 @@ - @photos.each do |p| .col-md-2.six-across .thumbnail(style='height: 220px') - = link_to image_tag(FlickRaw.url_q(p), :alt => '', :class => 'img-rounded'), photos_path(:photo => { :flickr_photo_id => p.id }, :planting_id => @planting_id), :method => :post + = link_to image_tag(FlickRaw.url_q(p), :alt => '', :class => 'img-rounded'), photos_path(:photo => { :flickr_photo_id => p.id }, :type => @type, :id => @id), :method => :post %p =p.title diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index a6a713229..a5f8c8b8e 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -57,4 +57,4 @@ .col-md-2 .thumbnail(style='height: 220px') %p{:style => 'text-align: center; padding-top: 50px'} - = link_to "Add photo", new_photo_path(:planting_id => @planting.id), :class => 'btn btn-primary' + = link_to "Add photo", new_photo_path(:type => "planting", :id => @planting.id), :class => 'btn btn-primary' diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index eb51fab96..92d55dfaa 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -6,7 +6,8 @@ Devise.setup do |config| # note that it will be overwritten if you use your own mailer class with default "from" parameter. config.mailer_sender = "Growstuff " - config.secret_key = ENV['RAILS_SECRET_TOKEN'] + #config.secret_key = ENV['RAILS_SECRET_TOKEN'] + config.secret_key = '6ccb49df7c02ce710c37ec3eadd81ec65e732708bd859ce6076f42593f0cff186b7c35be2fd195f46c9e923296ee07bf309eb33866fe1ea7d9ffeb88f367489c' # Configure the class responsible to send e-mails. # config.mailer = "Devise::Mailer" diff --git a/db/schema.rb b/db/schema.rb index 4c5ee003b..4ac5f92c7 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140720085713) do +ActiveRecord::Schema.define(:version => 20140905001730) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -108,6 +108,11 @@ ActiveRecord::Schema.define(:version => 20140720085713) do t.integer "plant_part_id" end + create_table "harvests_photos", :id => false, :force => true do |t| + t.integer "photo_id" + t.integer "harvest_id" + end + create_table "members", :force => true do |t| t.string "email", :default => "", :null => false t.string "encrypted_password", :default => "", :null => false diff --git a/spec/controllers/photos_controller_spec.rb b/spec/controllers/photos_controller_spec.rb index a73981e4c..4370e626f 100644 --- a/spec/controllers/photos_controller_spec.rb +++ b/spec/controllers/photos_controller_spec.rb @@ -37,8 +37,15 @@ describe PhotosController do end it "assigns a planting id" do - get :new, { :planting_id => 5 } - assigns(:planting_id).should eq "5" + get :new, { :type => "planting", :id => 5 } + assigns(:id).should eq "5" + assigns(:type).should eq "planting" + end + + it "assigns a harvest id" do + get :new, { :type => "harvest", :id => 5 } + assigns(:id).should eq "5" + assigns(:type).should eq "harvest" end it "assigns the current set as @current_set" do @@ -69,7 +76,8 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :planting_id => planting.id } + :type => "planting", + :id => planting.id } Photo.last.plantings.first.should eq planting end @@ -80,12 +88,39 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :planting_id => planting.id } + :type => "planting", + :id => planting.id } post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :planting_id => planting.id } + :type => "planting", + :id => planting.id } Photo.last.plantings.count.should eq 1 end - end + + it "attaches the photo to a harvest" do + member = FactoryGirl.create(:member) + controller.stub(:current_member) { member } + harvest = FactoryGirl.create(:harvest, :owner => member) + photo = FactoryGirl.create(:photo, :owner => member) + post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, + :type => "harvest", + :id => harvest.id } + Photo.last.harvests.first.should eq harvest + end + + it "doesn't attach a photo to a harvest twice" do + member = FactoryGirl.create(:member) + controller.stub(:current_member) { member } + harvest = FactoryGirl.create(:harvest, :owner => member) + photo = FactoryGirl.create(:photo, :owner => member) + post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, + :type => "harvest", + :id => harvest.id } + post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, + :type => "harvest", + :id => harvest.id } + Photo.last.harvests.count.should eq 1 + end + end describe "for the second time" do it "does not add a photo twice" do @@ -106,9 +141,21 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :planting_id => planting.id } + :type => "planting", + :id => planting.id } Photo.last.plantings.first.should eq planting end + + it "creates the harvest/photo link" do + member = FactoryGirl.create(:member) + controller.stub(:current_member) { member } + harvest = FactoryGirl.create(:harvest, :owner => member) + photo = FactoryGirl.create(:photo, :owner => member) + post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, + :type => "harvest", + :id => harvest.id } + Photo.last.harvests.first.should eq harvest + end end describe "with mismatched owners" do @@ -117,9 +164,20 @@ describe PhotosController do planting = FactoryGirl.create(:planting) photo = FactoryGirl.create(:photo) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :planting_id => planting.id } + :type => "planting", + :id => planting.id } Photo.last.plantings.first.should_not eq planting end + + it "creates the harvest/photo link" do + # members will be auto-created, and different + harvest = FactoryGirl.create(:harvest) + photo = FactoryGirl.create(:photo) + post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, + :type => "harvest", + :id => harvest.id } + Photo.last.harvests.first.should_not eq harvest + end end end end diff --git a/spec/models/harvest_spec.rb b/spec/models/harvest_spec.rb index ca0180ca1..a5b5cabf7 100644 --- a/spec/models/harvest_spec.rb +++ b/spec/models/harvest_spec.rb @@ -125,4 +125,32 @@ describe Harvest do Harvest.all.should eq [@h2, @h1] end end + + context 'photos' do + before(:each) do + @harvest = FactoryGirl.create(:harvest) + @photo = FactoryGirl.create(:photo) + @harvest.photos << @photo + end + + it 'has a photo' do + @harvest.photos.first.should eq @photo + end + + it 'deletes association with photos when photo is deleted' do + @photo.destroy + @harvest.reload + @harvest.photos.should be_empty + end + + it 'has a default photo' do + @harvest.default_photo.should eq @photo + end + + it 'chooses the most recent photo' do + @photo2 = FactoryGirl.create(:photo) + @harvest.photos << @photo2 + @harvest.default_photo.should eq @photo2 + end + end end From 2b7b5bad6378edb66a0b9307c3c33b89c10f4b4e Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Tue, 9 Sep 2014 20:21:16 -0400 Subject: [PATCH 005/132] including all the new files needed for harvest photos, this time --- .../harvests/_image_with_popover.html.haml | 12 ++++++ app/views/harvests/_thumbnail.html.haml | 37 +++++++++++++++++++ ...0140905001730_add_harvests_photos_table.rb | 8 ++++ 3 files changed, 57 insertions(+) create mode 100644 app/views/harvests/_image_with_popover.html.haml create mode 100644 app/views/harvests/_thumbnail.html.haml create mode 100644 db/migrate/20140905001730_add_harvests_photos_table.rb diff --git a/app/views/harvests/_image_with_popover.html.haml b/app/views/harvests/_image_with_popover.html.haml new file mode 100644 index 000000000..5eef8610e --- /dev/null +++ b/app/views/harvests/_image_with_popover.html.haml @@ -0,0 +1,12 @@ +- cache "planting_image_#{planting.id}" do + = link_to | + image_tag( | + planting.photos.present? ? planting.photos.first.thumbnail_url : 'placeholder_150.png', | + :alt => planting.to_s | + ), | + planting, | + :rel => "popover", | + 'data-trigger' => 'hover', | + 'data-title' => planting.to_s, | + 'data-content' => "#{ render :partial => 'plantings/popover', :locals => { :planting => planting } }", | + 'data-html' => true diff --git a/app/views/harvests/_thumbnail.html.haml b/app/views/harvests/_thumbnail.html.haml new file mode 100644 index 000000000..f3b1c1211 --- /dev/null +++ b/app/views/harvests/_thumbnail.html.haml @@ -0,0 +1,37 @@ +.well + .row-fluid + .span3 + = link_to image_tag((planting.default_photo ? planting.default_photo.thumbnail_url : 'placeholder_150.png'), :alt => '', :class => 'img-rounded'), planting + + .span9 + %h4 + - if defined?(title) && title == 'owner' + = link_to planting.owner, planting.owner + - else + = link_to planting.crop.name, planting + + %p + Planted + - if planting.planted_at + = planting.planted_at + in + = link_to planting.location, planting.garden + + %p + - if planting.quantity + Quantity: + = planting.quantity + - else +   + + - if planting.description && ! defined?(hide_description) + %div + :growstuff_markdown + #{ planting.description } + + - if can? :edit, planting or can? :destroy, planting + %p + - if can? :edit, planting + =link_to 'Edit', edit_planting_path(planting), :class => 'btn btn-mini' + - if can? :destroy, planting + =link_to 'Delete', planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-mini' diff --git a/db/migrate/20140905001730_add_harvests_photos_table.rb b/db/migrate/20140905001730_add_harvests_photos_table.rb new file mode 100644 index 000000000..edacd061b --- /dev/null +++ b/db/migrate/20140905001730_add_harvests_photos_table.rb @@ -0,0 +1,8 @@ +class AddHarvestsPhotosTable < ActiveRecord::Migration + def change + create_table :harvests_photos, :id => false do |t| + t.integer :photo_id + t.integer :harvest_id + end + end +end From ed537e583baa711f64e48eb2d744a54e1cfbfc89 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Tue, 9 Sep 2014 20:31:14 -0400 Subject: [PATCH 006/132] whitespace fixes and um oops no there shouldn't be a secret key in that config file --- app/controllers/photos_controller.rb | 48 +++++++++++----------- app/models/photo.rb | 4 +- config/initializers/devise.rb | 3 +- spec/controllers/photos_controller_spec.rb | 28 ++++++------- 4 files changed, 41 insertions(+), 42 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 8932ebae8..f4004ad73 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -64,32 +64,32 @@ class PhotosController < ApplicationController @photo.owner_id = current_member.id @photo.set_flickr_metadata - # several models can have photos. we need to know what model and the id - # for the entry to attach the photo to - valid_models = ["planting", "harvest"] + # several models can have photos. we need to know what model and the id + # for the entry to attach the photo to + valid_models = ["planting", "harvest"] if params[:type] if valid_models.include?(params[:type]) - if params[:id] - item = params[:type].camelcase.constantize.find_by_id(params[:id]) - if item - if item.owner.id == current_member.id - # This syntax is weird, so just know that it means this: - # @photo.harvests << item unless @photo.harvests.include?(item) - # but with the correct many-to-many relationship automatically referenced - (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) - else - flash[:alert] = "You must own both the #{params[:type]} and the photo." - end - else - flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." - end - else - flash[:alert] = "Missing id parameter" - end - else - flash[:alert] = "Cannot attach photos to #{params[:type]}" - end - else + if params[:id] + item = params[:type].camelcase.constantize.find_by_id(params[:id]) + if item + if item.owner.id == current_member.id + # This syntax is weird, so just know that it means this: + # @photo.harvests << item unless @photo.harvests.include?(item) + # but with the correct many-to-many relationship automatically referenced + (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) + else + flash[:alert] = "You must own both the #{params[:type]} and the photo." + end + else + flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." + end + else + flash[:alert] = "Missing id parameter" + end + else + flash[:alert] = "Cannot attach photos to #{params[:type]}" + end + else flash[:alert] = "Missing type parameter" end diff --git a/app/models/photo.rb b/app/models/photo.rb index aa5a624d6..4f0a0160f 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -6,8 +6,8 @@ class Photo < ActiveRecord::Base has_and_belongs_to_many :plantings has_and_belongs_to_many :harvests before_destroy do |photo| - photo.plantings.clear - photo.harvests.clear + photo.plantings.clear + photo.harvests.clear end default_scope order("created_at desc") diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index 92d55dfaa..eb51fab96 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -6,8 +6,7 @@ Devise.setup do |config| # note that it will be overwritten if you use your own mailer class with default "from" parameter. config.mailer_sender = "Growstuff " - #config.secret_key = ENV['RAILS_SECRET_TOKEN'] - config.secret_key = '6ccb49df7c02ce710c37ec3eadd81ec65e732708bd859ce6076f42593f0cff186b7c35be2fd195f46c9e923296ee07bf309eb33866fe1ea7d9ffeb88f367489c' + config.secret_key = ENV['RAILS_SECRET_TOKEN'] # Configure the class responsible to send e-mails. # config.mailer = "Devise::Mailer" diff --git a/spec/controllers/photos_controller_spec.rb b/spec/controllers/photos_controller_spec.rb index 4370e626f..1c3b47780 100644 --- a/spec/controllers/photos_controller_spec.rb +++ b/spec/controllers/photos_controller_spec.rb @@ -39,13 +39,13 @@ describe PhotosController do it "assigns a planting id" do get :new, { :type => "planting", :id => 5 } assigns(:id).should eq "5" - assigns(:type).should eq "planting" + assigns(:type).should eq "planting" end it "assigns a harvest id" do get :new, { :type => "harvest", :id => 5 } assigns(:id).should eq "5" - assigns(:type).should eq "harvest" + assigns(:type).should eq "harvest" end it "assigns the current set as @current_set" do @@ -76,7 +76,7 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "planting", + :type => "planting", :id => planting.id } Photo.last.plantings.first.should eq planting end @@ -88,10 +88,10 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "planting", + :type => "planting", :id => planting.id } post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "planting", + :type => "planting", :id => planting.id } Photo.last.plantings.count.should eq 1 end @@ -102,7 +102,7 @@ describe PhotosController do harvest = FactoryGirl.create(:harvest, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "harvest", + :type => "harvest", :id => harvest.id } Photo.last.harvests.first.should eq harvest end @@ -113,14 +113,14 @@ describe PhotosController do harvest = FactoryGirl.create(:harvest, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "harvest", + :type => "harvest", :id => harvest.id } post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "harvest", + :type => "harvest", :id => harvest.id } Photo.last.harvests.count.should eq 1 - end - end + end + end describe "for the second time" do it "does not add a photo twice" do @@ -141,7 +141,7 @@ describe PhotosController do planting = FactoryGirl.create(:planting, :garden => garden, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "planting", + :type => "planting", :id => planting.id } Photo.last.plantings.first.should eq planting end @@ -152,7 +152,7 @@ describe PhotosController do harvest = FactoryGirl.create(:harvest, :owner => member) photo = FactoryGirl.create(:photo, :owner => member) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "harvest", + :type => "harvest", :id => harvest.id } Photo.last.harvests.first.should eq harvest end @@ -164,7 +164,7 @@ describe PhotosController do planting = FactoryGirl.create(:planting) photo = FactoryGirl.create(:photo) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "planting", + :type => "planting", :id => planting.id } Photo.last.plantings.first.should_not eq planting end @@ -174,7 +174,7 @@ describe PhotosController do harvest = FactoryGirl.create(:harvest) photo = FactoryGirl.create(:photo) post :create, {:photo => { :flickr_photo_id => photo.flickr_photo_id }, - :type => "harvest", + :type => "harvest", :id => harvest.id } Photo.last.harvests.first.should_not eq harvest end From 550f3c53265e80185486298079fdb7bebde63fc0 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Tue, 9 Sep 2014 22:18:43 -0400 Subject: [PATCH 007/132] removing unneeded photo-related files --- .../harvests/_image_with_popover.html.haml | 12 ------ app/views/harvests/_thumbnail.html.haml | 37 ------------------- 2 files changed, 49 deletions(-) delete mode 100644 app/views/harvests/_image_with_popover.html.haml delete mode 100644 app/views/harvests/_thumbnail.html.haml diff --git a/app/views/harvests/_image_with_popover.html.haml b/app/views/harvests/_image_with_popover.html.haml deleted file mode 100644 index 5eef8610e..000000000 --- a/app/views/harvests/_image_with_popover.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- cache "planting_image_#{planting.id}" do - = link_to | - image_tag( | - planting.photos.present? ? planting.photos.first.thumbnail_url : 'placeholder_150.png', | - :alt => planting.to_s | - ), | - planting, | - :rel => "popover", | - 'data-trigger' => 'hover', | - 'data-title' => planting.to_s, | - 'data-content' => "#{ render :partial => 'plantings/popover', :locals => { :planting => planting } }", | - 'data-html' => true diff --git a/app/views/harvests/_thumbnail.html.haml b/app/views/harvests/_thumbnail.html.haml deleted file mode 100644 index f3b1c1211..000000000 --- a/app/views/harvests/_thumbnail.html.haml +++ /dev/null @@ -1,37 +0,0 @@ -.well - .row-fluid - .span3 - = link_to image_tag((planting.default_photo ? planting.default_photo.thumbnail_url : 'placeholder_150.png'), :alt => '', :class => 'img-rounded'), planting - - .span9 - %h4 - - if defined?(title) && title == 'owner' - = link_to planting.owner, planting.owner - - else - = link_to planting.crop.name, planting - - %p - Planted - - if planting.planted_at - = planting.planted_at - in - = link_to planting.location, planting.garden - - %p - - if planting.quantity - Quantity: - = planting.quantity - - else -   - - - if planting.description && ! defined?(hide_description) - %div - :growstuff_markdown - #{ planting.description } - - - if can? :edit, planting or can? :destroy, planting - %p - - if can? :edit, planting - =link_to 'Edit', edit_planting_path(planting), :class => 'btn btn-mini' - - if can? :destroy, planting - =link_to 'Delete', planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-mini' From 2bbccfc44da1d8cb070c9cad5e9099915fb0ba18 Mon Sep 17 00:00:00 2001 From: Skud Date: Fri, 12 Sep 2014 12:30:04 +1000 Subject: [PATCH 008/132] Added tests for planting reminder email --- spec/mailers/notifier_spec.rb | 39 ++++++++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/spec/mailers/notifier_spec.rb b/spec/mailers/notifier_spec.rb index 3b4aa3ddb..6ade0fb78 100644 --- a/spec/mailers/notifier_spec.rb +++ b/spec/mailers/notifier_spec.rb @@ -2,22 +2,45 @@ require "spec_helper" describe Notifier do - describe "notify" do - before(:each) do - @notification = FactoryGirl.create(:notification) - @mail = Notifier.notify(@notification) - end + describe "notifications" do + let(:notification) { FactoryGirl.create(:notification) } + let(:mail) { Notifier.notify(notification) } it 'sets the subject correctly' do - @mail.subject.should == @notification.subject + mail.subject.should == notification.subject end it 'comes from noreply@growstuff.org' do - @mail.from.should == ['noreply@growstuff.org'] + mail.from.should == ['noreply@growstuff.org'] end it 'sends the mail to the recipient of the notification' do - @mail.to.should == [@notification.recipient.email] + mail.to.should == [notification.recipient.email] + end + end + + describe "planting reminders" do + let(:member) { FactoryGirl.create(:member) } + let(:mail) { Notifier.planting_reminder(member) } + + it 'sets the subject correctly' do + mail.subject.should == "What have you planted lately?" + end + + it 'comes from noreply@growstuff.org' do + mail.from.should == ['noreply@growstuff.org'] + end + + it 'sends the mail to the recipient of the notification' do + mail.to.should == [member.email] + end + + it 'includes the new planting URL' do + mail.body.encoded.should match new_planting_path + end + + it 'includes the new harvest URL' do + mail.body.encoded.should match new_harvest_path end end From 09bef3f9d8bb889551aff44c5cf94e17382f2db5 Mon Sep 17 00:00:00 2001 From: Skud Date: Fri, 12 Sep 2014 12:30:27 +1000 Subject: [PATCH 009/132] Added options for if you didn't plant/harvest anything --- .../notifier/planting_reminder.html.haml | 74 ++++++++++++------- 1 file changed, 47 insertions(+), 27 deletions(-) diff --git a/app/views/notifier/planting_reminder.html.haml b/app/views/notifier/planting_reminder.html.haml index 1e77fc195..98ca52fa9 100644 --- a/app/views/notifier/planting_reminder.html.haml +++ b/app/views/notifier/planting_reminder.html.haml @@ -3,40 +3,60 @@ %h2 What's new in your garden? -Have you planted anything recently? The most recent plantings you've -told us about are: - -%ul -- @plantings.each do |p| - %li - = link_to p, planting_url(p) - planted - = distance_of_time_in_words(p.created_at, Time.zone.now) - ago. - %p - Planted anything new? - = link_to "Track your plantings here.", new_planting_url + Have you planted anything recently? + +- if @member.plantings.size == 0 + %p + #{ENV['GROWSTUFF_SITE_NAME']} lets you track what food you're growing + in your garden. + %p + = link_to "Get started now.", new_planting_url + +- else + %p + The most recent plantings you've told us about are: + + %ul + - @plantings.each do |p| + %li + = link_to p, planting_url(p) + planted + = distance_of_time_in_words(p.created_at, Time.zone.now) + ago. + + %p + Planted anything new? + = link_to "Track your plantings here.", new_planting_url %h2 Your recent harvests -According to our records, the last few things you harvested were: +- if @member.harvests.size == 0 + %p + With #{ENV['GROWSTUFF_SITE_NAME']} you can keep track of what you + harvest from your garden. -%ul -- @harvests.each do |h| - %li - = link_to h, harvest_url(h) - harvested - = distance_of_time_in_words(h.created_at, Time.zone.now) - ago. + %p + = link_to "Get started now.", new_harvest_url + +- else + According to our records, the last few things you harvested were: + + %ul + - @harvests.each do |h| + %li + = link_to h, harvest_url(h) + harvested + = distance_of_time_in_words(h.created_at, Time.zone.now) + ago. + + %p + Harvested anything else lately? + = link_to "Track your harvests here.", new_harvest_url %p - Harvested anything else lately? - = link_to "Track your harvests here.", new_harvest_url - -%p - Don't want to get these emails any more? - = link_to "Turn off these notifications", edit_member_registration_url +Don't want to get these emails any more? += link_to "Turn off these notifications", edit_member_registration_url %p The #{site_name} team. From 3cf2a50a34ffac3e4917b3c8de4c0dd7d102e730 Mon Sep 17 00:00:00 2001 From: Skud Date: Fri, 12 Sep 2014 13:20:41 +1000 Subject: [PATCH 010/132] modified Planting.interesting to have some options re: howmany and photos --- app/models/planting.rb | 8 ++-- spec/models/planting_spec.rb | 77 ++++++++++++++++++++++++------------ 2 files changed, 55 insertions(+), 30 deletions(-) diff --git a/app/models/planting.rb b/app/models/planting.rb index b848d3490..78dcc82b2 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -78,15 +78,15 @@ class Planting < ActiveRecord::Base # return a list of interesting plantings, for the homepage etc. # we can't do this via a scope (as far as we know) so sadly we have to # do it this way. - def Planting.interesting - howmany = 12 # max amount to collect - + def Planting.interesting(howmany=12, require_photo=true) interesting_plantings = Array.new seen_owners = Hash.new(false) # keep track of which owners we've seen already Planting.all.each do |p| break if interesting_plantings.count == howmany # got enough yet? - next unless p.interesting? # skip those that don't have photos + if require_photo + next unless p.photos.present? # skip those without photos, if required + end next if seen_owners[p.owner] # skip if we already have one from this owner seen_owners[p.owner] = true # we've seen this owner interesting_plantings.push(p) diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index eae16cbe5..f791bf5af 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -153,7 +153,7 @@ describe Planting do end end - context 'interesting crops' do + context 'interesting plantings' do it 'picks up interesting plantings' do # plantings have members created implicitly for them # each member is different, hence these are all interesting @@ -177,38 +177,63 @@ describe Planting do ] end - it 'ignores plantings without photos' do - # first, an interesting planting - @planting = FactoryGirl.create(:planting) - @planting.photos << FactoryGirl.create(:photo) - @planting.save + context "default arguments" do + it 'ignores plantings without photos' do + # first, an interesting planting + @planting = FactoryGirl.create(:planting) + @planting.photos << FactoryGirl.create(:photo) + @planting.save - # this one doesn't have a photo - @boring_planting = FactoryGirl.create(:planting) + # this one doesn't have a photo + @boring_planting = FactoryGirl.create(:planting) - Planting.interesting.should include @planting - Planting.interesting.should_not include @boring_planting + Planting.interesting.should include @planting + Planting.interesting.should_not include @boring_planting + end + + it 'ignores plantings with the same owner' do + # this planting is older + @planting1 = FactoryGirl.create(:planting, :created_at => 1.day.ago) + @planting1.photos << FactoryGirl.create(:photo) + @planting1.save + + # this one is newer, and has the same owner, through the garden + @planting2 = FactoryGirl.create(:planting, + :created_at => 1.minute.ago, + :owner_id => @planting1.owner.id + ) + @planting2.photos << FactoryGirl.create(:photo) + @planting2.save + + # result: the newer one is interesting, the older one isn't + Planting.interesting.should include @planting2 + Planting.interesting.should_not include @planting1 + end end - it 'ignores plantings with the same owner' do - # this planting is older - @planting1 = FactoryGirl.create(:planting, :created_at => 1.day.ago) - @planting1.photos << FactoryGirl.create(:photo) - @planting1.save + context "with require_photo = false" do + it "returns plantings without photos" do + # first, a planting with a photo + @planting = FactoryGirl.create(:planting) + @planting.photos << FactoryGirl.create(:photo) + @planting.save - # this one is newer, and has the same owner, through the garden - @planting2 = FactoryGirl.create(:planting, - :created_at => 1.minute.ago, - :owner_id => @planting1.owner.id - ) - @planting2.photos << FactoryGirl.create(:photo) - @planting2.save + # this one doesn't have a photo + @boring_planting = FactoryGirl.create(:planting) - # result: the newer one is interesting, the older one isn't - Planting.interesting.should include @planting2 - Planting.interesting.should_not include @planting1 + interesting = Planting.interesting(10, false) + interesting.should include @planting + interesting.should include @boring_planting + end end - end + context "with howmany argument" do + it "only returns the number asked for" do + @plantings = FactoryGirl.create_list(:planting, 10) + Planting.interesting(3, false).length.should eq 3 + end + end + + end # interesting plantings end From eeb2bf9938192925951530d108692bcab6b632b3 Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Wed, 17 Sep 2014 21:38:43 +1000 Subject: [PATCH 011/132] added planting_id param to Flickr set search (new_photo_path form). --- app/views/photos/new.html.haml | 1 + 1 file changed, 1 insertion(+) diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index 32ea66915..365351fac 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -15,6 +15,7 @@ = form_tag(new_photo_path, :method => :get, :class => 'form-inline') do = label_tag :set, "Choose a photo set:", :class => 'control-label' = select_tag :set, options_for_select(@sets, @current_set), :class => 'input-large' + = hidden_field_tag :planting_id, @planting_id = submit_tag "Search", :class => 'btn btn-primary' %div.pagination From f355c393f006c596c3187a879960269f770dc547 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 18 Sep 2014 06:15:59 +1000 Subject: [PATCH 012/132] basic setup --- app/controllers/application_controller.rb | 10 ++ app/views/home/_blurb.html.haml | 5 +- config/initializers/locale.rb | 2 + config/locales/en.yml | 1 + config/locales/jp.yml | 2 + config/routes.rb | 137 +++++++++++----------- db/schema.rb | 1 + 7 files changed, 87 insertions(+), 71 deletions(-) create mode 100644 config/initializers/locale.rb create mode 100644 config/locales/jp.yml diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 0d07cb866..29f5adb35 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -4,6 +4,7 @@ class ApplicationController < ActionController::Base include ApplicationHelper after_filter :store_location + before_filter :set_locale def store_location if (request.path != "/members/sign_in" && @@ -32,5 +33,14 @@ class ApplicationController < ActionController::Base rescue_from CanCan::AccessDenied do |exception| redirect_to request.referer || root_url, :alert => exception.message end + + def set_locale + I18n.locale = params[:locale] || I18n.default_locale + end + + def default_url_options(options={}) + logger.debug "default_url_options is passed options: #{options.inspect}\n" + { locale: I18n.locale } + end end diff --git a/app/views/home/_blurb.html.haml b/app/views/home/_blurb.html.haml index 3e742b8d4..ba3fd072f 100644 --- a/app/views/home/_blurb.html.haml +++ b/app/views/home/_blurb.html.haml @@ -5,10 +5,7 @@ .row .col-md-8.info %p - #{ENV['GROWSTUFF_SITE_NAME']} is a community of food gardeners. - We're building an open source platform to help you learn about - growing food, track what you plant and harvest, and swap - seeds and produce with other gardeners near you. + #{ENV['GROWSTUFF_SITE_NAME']} #{t 'blurb'} = render :partial => 'stats' .col-md-4.signup diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb new file mode 100644 index 000000000..ef55314c4 --- /dev/null +++ b/config/initializers/locale.rb @@ -0,0 +1,2 @@ +I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] + \ No newline at end of file diff --git a/config/locales/en.yml b/config/locales/en.yml index 7a30c72fe..4d47f61ab 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,3 +13,4 @@ en: all: "Please sign in or sign up to create a %{subject}." manage: all: "Not authorized to %{action} %{subject}." + blurb: "is a community of food gardeners. We're building an open source platform to help you learn about growing food, track what you plant and harvest, and swap seeds and produce with other gardeners near you." \ No newline at end of file diff --git a/config/locales/jp.yml b/config/locales/jp.yml new file mode 100644 index 000000000..89dcea14a --- /dev/null +++ b/config/locales/jp.yml @@ -0,0 +1,2 @@ +jp: + blurb: "はガーデナーのコミュニティーです。" diff --git a/config/routes.rb b/config/routes.rb index 360ed2477..01600b7ed 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,78 +1,81 @@ Growstuff::Application.routes.draw do - resources :plant_parts - - - devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } - resources :members - - resources :photos - - resources :authentications - - resources :plantings - match '/plantings/owner/:owner' => 'plantings#index', :as => 'plantings_by_owner' - - resources :gardens - match '/gardens/owner/:owner' => 'gardens#index', :as => 'gardens_by_owner' - - resources :seeds - match '/seeds/owner/:owner' => 'seeds#index', :as => 'seeds_by_owner' - - resources :harvests - match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' - - resources :posts - match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' - - resources :scientific_names - - match 'crops/wrangle' => 'crops#wrangle', :as => 'wrangle_crops' - match 'crops/hierarchy' => 'crops#hierarchy', :as => 'crops_hierarchy' - match 'crops/search' => 'crops#search', :as => 'crops_search' - resources :crops - - resources :comments - resources :roles - resources :forums - resources :notifications - - get '/places' => 'places#index' - get '/places/search' => 'places#search', :as => 'search_places' - get '/places/:place' => 'places#show', :as => 'place' - - # everything for paid accounts etc - resources :account_types - resources :accounts - resources :orders - match 'orders/:id/checkout' => 'orders#checkout', :as => 'checkout_order' - match 'orders/:id/complete' => 'orders#complete', :as => 'complete_order' - match 'orders/:id/cancel' => 'orders#cancel', :as => 'cancel_order' - - resources :order_items - resources :products - - get "home/index" + get "/:locale" => 'home#index' root :to => 'home#index' - match 'auth/:provider/callback' => 'authentications#create' + scope '/:locale' do + + resources :plant_parts + + devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } + resources :members + + resources :photos + + resources :authentications + + resources :plantings + match '/plantings/owner/:owner' => 'plantings#index', :as => 'plantings_by_owner' + + resources :gardens + match '/gardens/owner/:owner' => 'gardens#index', :as => 'gardens_by_owner' + + resources :seeds + match '/seeds/owner/:owner' => 'seeds#index', :as => 'seeds_by_owner' + + resources :harvests + match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' + + resources :posts + match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' + + resources :scientific_names + + match 'crops/wrangle' => 'crops#wrangle', :as => 'wrangle_crops' + match 'crops/hierarchy' => 'crops#hierarchy', :as => 'crops_hierarchy' + match 'crops/search' => 'crops#search', :as => 'crops_search' + resources :crops + + resources :comments + resources :roles + resources :forums + resources :notifications + + get '/places' => 'places#index' + get '/places/search' => 'places#search', :as => 'search_places' + get '/places/:place' => 'places#show', :as => 'place' + + # everything for paid accounts etc + resources :account_types + resources :accounts + resources :orders + match 'orders/:id/checkout' => 'orders#checkout', :as => 'checkout_order' + match 'orders/:id/complete' => 'orders#complete', :as => 'complete_order' + match 'orders/:id/cancel' => 'orders#cancel', :as => 'cancel_order' + + resources :order_items + resources :products + + match 'auth/:provider/callback' => 'authentications#create' - match '/policy/:action' => 'policy#:action' + match '/policy/:action' => 'policy#:action' - match '/support' => 'support#index' - match '/support/:action' => 'support#:action' + match '/support' => 'support#index' + match '/support/:action' => 'support#:action' - match '/about' => 'about#index' - match '/about/:action' => 'about#:action' + match '/about' => 'about#index' + match '/about/:action' => 'about#:action' - match '/shop' => 'shop#index' - match '/shop/:action' => 'shop#:action' + match '/shop' => 'shop#index' + match '/shop/:action' => 'shop#:action' - match '/admin/orders' => 'admin/orders#index' - match '/admin/orders/:action' => 'admin/orders#:action' - match '/admin' => 'admin#index' - match '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter - match '/admin/:action' => 'admin#:action' + match '/admin/orders' => 'admin/orders#index' + match '/admin/orders/:action' => 'admin/orders#:action' + match '/admin' => 'admin#index' + match '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter + match '/admin/:action' => 'admin#:action' -end + end + +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 29271f5e6..af91a7a9d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -139,6 +139,7 @@ ActiveRecord::Schema.define(:version => 20140829230600) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" + t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true From 87df8661c7c6829549ef2e4bb5dc71d4af4527d2 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 18 Sep 2014 06:24:59 +1000 Subject: [PATCH 013/132] raise exception on missing translations in test and dev environment --- config/initializers/locale.rb | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index ef55314c4..eb4ed56e7 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,2 +1,8 @@ I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] + +if Rails.env.development? || Rails.env.test? + I18n.exception_handler = lambda do |exception, locale, key, options| + raise "Missing translation: #{key}" + end +end \ No newline at end of file From f8ee9d05894100a2b4041e23b5d154110e902f67 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 18 Sep 2014 07:14:35 +1000 Subject: [PATCH 014/132] don't scope routes to locale to make url prettier because it breaks tests like whoa --- Gemfile | 1 + Gemfile.lock | 18 +++++ config/i18n-tasks.yml | 90 ++++++++++++++++++++++ config/initializers/locale.rb | 1 + config/routes.rb | 137 +++++++++++++++++----------------- spec/i18n_spec.rb | 18 +++++ 6 files changed, 195 insertions(+), 70 deletions(-) create mode 100644 config/i18n-tasks.yml create mode 100644 spec/i18n_spec.rb diff --git a/Gemfile b/Gemfile index e47403546..fd8184894 100644 --- a/Gemfile +++ b/Gemfile @@ -127,4 +127,5 @@ group :development, :test do gem 'coveralls', require: false # coverage analysis gem 'capybara' # integration tests gem 'poltergeist', '~> 1.5.1' # for headless JS testing + gem 'i18n-tasks' # adds tests for finding missing and unused translations end diff --git a/Gemfile.lock b/Gemfile.lock index 7829a34de..d8ccbfcb2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -110,6 +110,10 @@ GEM warden (~> 1.2.3) diff-lcs (1.1.3) docile (1.1.5) + easy_translate (0.5.0) + json + thread + thread_safe erubis (2.7.0) execjs (2.2.1) factory_girl (4.4.0) @@ -138,11 +142,21 @@ GEM haml (>= 3.1, < 4.1) railties (>= 3.1, < 4.1) hashie (3.3.1) + highline (1.6.21) hike (1.2.3) httparty (0.11.0) multi_json (~> 1.0) multi_xml (>= 0.5.2) i18n (0.6.1) + i18n-tasks (0.7.6) + activesupport + easy_translate (>= 0.5.0) + erubis + highline + i18n + slop (>= 3.5.0) + term-ansicolor + terminal-table journey (1.0.4) jquery-rails (3.1.1) railties (>= 3.0, < 5.0) @@ -257,6 +271,7 @@ GEM multi_json simplecov-html (~> 0.8.0) simplecov-html (0.8.0) + slop (3.6.0) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) @@ -266,10 +281,12 @@ GEM sprockets (>= 1.0.2) term-ansicolor (1.3.0) tins (~> 1.0) + terminal-table (1.4.5) therubyracer (0.12.1) libv8 (~> 3.16.14.0) ref thor (0.19.1) + thread (0.1.4) thread_safe (0.3.4) tilt (1.4.1) tins (1.3.2) @@ -325,6 +342,7 @@ DEPENDENCIES gravatar-ultimate haml haml-rails + i18n-tasks jquery-rails jquery-ui-rails js-routes diff --git a/config/i18n-tasks.yml b/config/i18n-tasks.yml new file mode 100644 index 000000000..e1a1c35d9 --- /dev/null +++ b/config/i18n-tasks.yml @@ -0,0 +1,90 @@ +# i18n-tasks finds and manages missing and unused translations https://github.com/glebm/i18n-tasks + +base_locale: en +## i18n-tasks detects locales automatically from the existing locale files +## uncomment to set locales explicitly +# locales: [en, es, fr] + +## i18n-tasks report locale, default: en, available: en, ru +# internal_locale: ru + +# Read and write locale data +data: + ## by default, translation data are read from the file system, or you can provide a custom data adapter + # adapter: I18n::Tasks::Data::FileSystem + + # Locale files to read from + read: + - config/locales/%{locale}.yml + # - config/locales/*.%{locale}.yml + # - config/locales/**/*.%{locale}.yml + + # key => file routes, matched top to bottom + write: + ## E.g., write devise and simple form keys to their respective files + # - ['{devise, simple_form}.*', 'config/locales/\1.%{locale}.yml'] + # Catch-all + - config/locales/%{locale}.yml + # `i18n-tasks normalize -p` will force move the keys according to these rules + + # YAML / JSON serializer options, passed to load / dump / parse / serialize + yaml: + write: + # do not wrap lines at 80 characters + line_width: -1 + json: + write: + # pretty print JSON + indent: ' ' + space: ' ' + object_nl: "\n" + array_nl: "\n" + +# Find translate calls +search: + ## Default scanner finds t() and I18n.t() calls + # scanner: I18n::Tasks::Scanners::PatternWithScopeScanner + + ## Paths to search in, passed to File.find + paths: + - app/ + + ## Root for resolving relative keys (default) + # relative_roots: + # - app/views + + ## File.fnmatch patterns to exclude from search (default) + # exclude: ["*.jpg", "*.png", "*.gif", "*.svg", "*.ico", "*.eot", "*.ttf", "*.woff", "*.pdf"] + + ## Or, File.fnmatch patterns to include + # include: ["*.rb", "*.html.slim"] + + ## Lines starting with # or / are ignored by default + # ignore_lines: + # - "^\\s*[#/](?!\\si18n-tasks-use)" + +## Google Translate +# translation: +# # Get an API key and set billing info at https://code.google.com/apis/console to use Google Translate +# api_key: "AbC-dEf5" + +## Consider these keys not missing +# ignore_missing: +# - pagination.views.* + +## Consider these keys used +# ignore_unused: +# - 'simple_form.{yes,no}' +# - 'simple_form.{placeholders,hints,labels}.*' +# - 'simple_form.{error_notification,required}.:' + +## Exclude these keys from `i18n-tasks eq-base' report +# ignore_eq_base: +# all: +# - common.ok +# fr,es: +# - common.brand + +## Exclude these keys from all of the reports +# ignore: +# - kaminari.* diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index eb4ed56e7..21c2733cb 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,4 +1,5 @@ I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] +I18n.default_locale = :en if Rails.env.development? || Rails.env.test? I18n.exception_handler = lambda do |exception, locale, key, options| diff --git a/config/routes.rb b/config/routes.rb index 01600b7ed..360ed2477 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -1,81 +1,78 @@ Growstuff::Application.routes.draw do - get "/:locale" => 'home#index' + resources :plant_parts + + + devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } + resources :members + + resources :photos + + resources :authentications + + resources :plantings + match '/plantings/owner/:owner' => 'plantings#index', :as => 'plantings_by_owner' + + resources :gardens + match '/gardens/owner/:owner' => 'gardens#index', :as => 'gardens_by_owner' + + resources :seeds + match '/seeds/owner/:owner' => 'seeds#index', :as => 'seeds_by_owner' + + resources :harvests + match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' + + resources :posts + match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' + + resources :scientific_names + + match 'crops/wrangle' => 'crops#wrangle', :as => 'wrangle_crops' + match 'crops/hierarchy' => 'crops#hierarchy', :as => 'crops_hierarchy' + match 'crops/search' => 'crops#search', :as => 'crops_search' + resources :crops + + resources :comments + resources :roles + resources :forums + resources :notifications + + get '/places' => 'places#index' + get '/places/search' => 'places#search', :as => 'search_places' + get '/places/:place' => 'places#show', :as => 'place' + + # everything for paid accounts etc + resources :account_types + resources :accounts + resources :orders + match 'orders/:id/checkout' => 'orders#checkout', :as => 'checkout_order' + match 'orders/:id/complete' => 'orders#complete', :as => 'complete_order' + match 'orders/:id/cancel' => 'orders#cancel', :as => 'cancel_order' + + resources :order_items + resources :products + + get "home/index" root :to => 'home#index' - scope '/:locale' do - - resources :plant_parts - - devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } - resources :members - - resources :photos - - resources :authentications - - resources :plantings - match '/plantings/owner/:owner' => 'plantings#index', :as => 'plantings_by_owner' - - resources :gardens - match '/gardens/owner/:owner' => 'gardens#index', :as => 'gardens_by_owner' - - resources :seeds - match '/seeds/owner/:owner' => 'seeds#index', :as => 'seeds_by_owner' - - resources :harvests - match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' - - resources :posts - match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' - - resources :scientific_names - - match 'crops/wrangle' => 'crops#wrangle', :as => 'wrangle_crops' - match 'crops/hierarchy' => 'crops#hierarchy', :as => 'crops_hierarchy' - match 'crops/search' => 'crops#search', :as => 'crops_search' - resources :crops - - resources :comments - resources :roles - resources :forums - resources :notifications - - get '/places' => 'places#index' - get '/places/search' => 'places#search', :as => 'search_places' - get '/places/:place' => 'places#show', :as => 'place' - - # everything for paid accounts etc - resources :account_types - resources :accounts - resources :orders - match 'orders/:id/checkout' => 'orders#checkout', :as => 'checkout_order' - match 'orders/:id/complete' => 'orders#complete', :as => 'complete_order' - match 'orders/:id/cancel' => 'orders#cancel', :as => 'cancel_order' - - resources :order_items - resources :products - - match 'auth/:provider/callback' => 'authentications#create' + match 'auth/:provider/callback' => 'authentications#create' - match '/policy/:action' => 'policy#:action' + match '/policy/:action' => 'policy#:action' - match '/support' => 'support#index' - match '/support/:action' => 'support#:action' + match '/support' => 'support#index' + match '/support/:action' => 'support#:action' - match '/about' => 'about#index' - match '/about/:action' => 'about#:action' + match '/about' => 'about#index' + match '/about/:action' => 'about#:action' - match '/shop' => 'shop#index' - match '/shop/:action' => 'shop#:action' + match '/shop' => 'shop#index' + match '/shop/:action' => 'shop#:action' - match '/admin/orders' => 'admin/orders#index' - match '/admin/orders/:action' => 'admin/orders#:action' - match '/admin' => 'admin#index' - match '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter - match '/admin/:action' => 'admin#:action' + match '/admin/orders' => 'admin/orders#index' + match '/admin/orders/:action' => 'admin/orders#:action' + match '/admin' => 'admin#index' + match '/admin/newsletter' => 'admin#newsletter', :as => :admin_newsletter + match '/admin/:action' => 'admin#:action' - end - -end \ No newline at end of file +end diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb new file mode 100644 index 000000000..0475c9fcc --- /dev/null +++ b/spec/i18n_spec.rb @@ -0,0 +1,18 @@ +require 'spec_helper' +require 'i18n/tasks' + +describe 'I18n' do + let(:i18n) { I18n::Tasks::BaseTask.new } + let(:missing_keys) { i18n.missing_keys } + let(:unused_keys) { i18n.unused_keys } + + it 'does not have missing keys' do + expect(missing_keys).to be_empty, + "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" + end + + it 'does not have unused keys' do + expect(i18n.unused_keys).to be_empty, + "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" + end +end From dd75f2aef909ab533f7c8c93db591f16ebc4778d Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Thu, 18 Sep 2014 14:18:18 +1000 Subject: [PATCH 015/132] updated regex for crop in markdown --- lib/haml/filters/growstuff_markdown.rb | 2 +- spec/lib/haml/filters/growstuff_markdown_spec.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/haml/filters/growstuff_markdown.rb b/lib/haml/filters/growstuff_markdown.rb index 35333a256..7069590e7 100644 --- a/lib/haml/filters/growstuff_markdown.rb +++ b/lib/haml/filters/growstuff_markdown.rb @@ -7,7 +7,7 @@ module Haml::Filters def render(text) # turn [tomato](crop) into [tomato](http://growstuff.org/crops/tomato) - expanded = text.gsub(/\[(.*?)\]\(crop\)/) do |m| + expanded = text.gsub(/\[([^\[\]]+?)\]\(crop\)/) do |m| crop_str = $1 # find crop case-insensitively crop = Crop.where('lower(name) = ?', crop_str.downcase).first diff --git a/spec/lib/haml/filters/growstuff_markdown_spec.rb b/spec/lib/haml/filters/growstuff_markdown_spec.rb index 566740307..cc6a2af54 100644 --- a/spec/lib/haml/filters/growstuff_markdown_spec.rb +++ b/spec/lib/haml/filters/growstuff_markdown_spec.rb @@ -52,4 +52,14 @@ describe 'Haml::Filters::Growstuff_Markdown' do rendered.should match /#{output_link(@crop, 'ToMaTo')}/ end + + it "doesn't convert when it's not followed by '(crop)'" do + tomato = FactoryGirl.create(:tomato) + maize = FactoryGirl.create(:maize) + string = "[maize](http://example.com) #{input_link(tomato)}" + rendered = Haml::Filters::GrowstuffMarkdown.render(string) + rendered.should match /#{output_link(tomato)}/ + rendered.should_not match /#{output_link(maize)}/ + rendered.should match "maize" + end end From 16f4d2f80e105f18881238fa42baa69bdd25d612 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Fri, 19 Sep 2014 07:17:08 +1000 Subject: [PATCH 016/132] fix spec to check href with newly added locale query param --- app/controllers/application_controller.rb | 2 +- config/locales/{jp.yml => ja.yml} | 2 +- spec/features/crop_wranglers_spec.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename config/locales/{jp.yml => ja.yml} (94%) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 29f5adb35..91fc81413 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -38,8 +38,8 @@ class ApplicationController < ActionController::Base I18n.locale = params[:locale] || I18n.default_locale end + # Adds locale query parameter to every path / url helper def default_url_options(options={}) - logger.debug "default_url_options is passed options: #{options.inspect}\n" { locale: I18n.locale } end diff --git a/config/locales/jp.yml b/config/locales/ja.yml similarity index 94% rename from config/locales/jp.yml rename to config/locales/ja.yml index 89dcea14a..c213277b9 100644 --- a/config/locales/jp.yml +++ b/config/locales/ja.yml @@ -1,2 +1,2 @@ -jp: +ja: blurb: "はガーデナーのコミュニティーです。" diff --git a/spec/features/crop_wranglers_spec.rb b/spec/features/crop_wranglers_spec.rb index 80475bd18..327be7e51 100644 --- a/spec/features/crop_wranglers_spec.rb +++ b/spec/features/crop_wranglers_spec.rb @@ -19,7 +19,7 @@ feature "crop wranglers" do within '.crop_wranglers' do expect(page).to have_content 'Crop Wranglers:' crop_wranglers.each do |crop_wrangler| - page.should have_link crop_wrangler.login_name, :href => member_path(crop_wrangler) + page.should have_link crop_wrangler.login_name, :href => member_path(crop_wrangler, {locale: 'en'}) end end end From 983fa8ede04a81aa25d5426dd07b49850f22674d Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Fri, 19 Sep 2014 07:29:04 +1000 Subject: [PATCH 017/132] abstract strings out of blurb partial --- app/views/home/_blurb.html.haml | 11 +++++------ config/locales/en.yml | 9 ++++++++- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/app/views/home/_blurb.html.haml b/app/views/home/_blurb.html.haml index ba3fd072f..fe06dad9c 100644 --- a/app/views/home/_blurb.html.haml +++ b/app/views/home/_blurb.html.haml @@ -5,15 +5,14 @@ .row .col-md-8.info %p - #{ENV['GROWSTUFF_SITE_NAME']} #{t 'blurb'} + #{ENV['GROWSTUFF_SITE_NAME']} #{t('.intro')} = render :partial => 'stats' .col-md-4.signup - %p Join now for your free garden journal, seed sharing, forums, and more. - %p= link_to 'Sign up', new_member_registration_path, :class => 'btn btn-primary btn-lg' + %p= t('.perks') + %p= link_to "#{t('.sign_up')}", new_member_registration_path, :class => 'btn btn-primary btn-lg' %p %small - Or - = link_to "sign in", new_member_session_path - if you already have an account. + = t('.already') + = link_to "#{t('.sign_in')}", new_member_session_path diff --git a/config/locales/en.yml b/config/locales/en.yml index 4d47f61ab..591ddd0e8 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -13,4 +13,11 @@ en: all: "Please sign in or sign up to create a %{subject}." manage: all: "Not authorized to %{action} %{subject}." - blurb: "is a community of food gardeners. We're building an open source platform to help you learn about growing food, track what you plant and harvest, and swap seeds and produce with other gardeners near you." \ No newline at end of file + + home: + blurb: + intro: "is a community of food gardeners. We're building an open source platform to help you learn about growing food, track what you plant and harvest, and swap seeds and produce with other gardeners near you." + perks: "Join now for your free garden journal, seed sharing, forums, and more." + sign_up: "Sign up" + sign_in: "Sign in" + already: "Already have an account?" \ No newline at end of file From aa028db2dc649d90a0f2090da1904ad35bbe7c6d Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Wed, 17 Sep 2014 21:38:43 +1000 Subject: [PATCH 018/132] added planting_id param to Flickr set search (new_photo_path form). --- CONTRIBUTORS.md | 1 + app/views/photos/new.html.haml | 1 + 2 files changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5bd5cc601..79dfdcaa1 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -45,3 +45,4 @@ submit the change with your pull request. - Marlena Compton / [Marlena](https://github.com/marlena) - Elizabeth A. Kari / [catfriend](https://github.com/catfriend) - Cheri Allen / [cherimarie](https://github.com/cherimarie) +- Shiho Takagi / [oshiho3](https://github.com/oshiho3) diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index 32ea66915..365351fac 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -15,6 +15,7 @@ = form_tag(new_photo_path, :method => :get, :class => 'form-inline') do = label_tag :set, "Choose a photo set:", :class => 'control-label' = select_tag :set, options_for_select(@sets, @current_set), :class => 'input-large' + = hidden_field_tag :planting_id, @planting_id = submit_tag "Search", :class => 'btn btn-primary' %div.pagination From 60354264d6643a6d268cb87efb83ade72444517a Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Fri, 19 Sep 2014 16:00:25 +1000 Subject: [PATCH 019/132] Included PT bug# to its test description. Test using the case from the bug --- spec/lib/haml/filters/growstuff_markdown_spec.rb | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/spec/lib/haml/filters/growstuff_markdown_spec.rb b/spec/lib/haml/filters/growstuff_markdown_spec.rb index cc6a2af54..718645322 100644 --- a/spec/lib/haml/filters/growstuff_markdown_spec.rb +++ b/spec/lib/haml/filters/growstuff_markdown_spec.rb @@ -52,14 +52,12 @@ describe 'Haml::Filters::Growstuff_Markdown' do rendered.should match /#{output_link(@crop, 'ToMaTo')}/ end - - it "doesn't convert when it's not followed by '(crop)'" do + it "fixes PT bug #78615258 (Markdown rendering bug with URLs and crops in same text)" do tomato = FactoryGirl.create(:tomato) - maize = FactoryGirl.create(:maize) - string = "[maize](http://example.com) #{input_link(tomato)}" + string = "[test](http://example.com) [tomato](crop)" rendered = Haml::Filters::GrowstuffMarkdown.render(string) rendered.should match /#{output_link(tomato)}/ - rendered.should_not match /#{output_link(maize)}/ - rendered.should match "maize" + rendered.should match "test" end + end From e3f590ede5d1cfbf2f0839a039c7f8f52931c6c1 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sat, 20 Sep 2014 21:47:18 +1000 Subject: [PATCH 020/132] abstract strings in home views into locale file --- app/views/home/_blurb.html.haml | 8 ++- app/views/home/_crops.html.haml | 8 +-- app/views/home/_discuss.html.haml | 6 +-- app/views/home/_keep_in_touch.html.haml | 13 ++--- app/views/home/_members.html.haml | 4 +- app/views/home/_open.html.haml | 40 ++++---------- app/views/home/_seeds.html.haml | 18 +++---- app/views/home/_stats.html.haml | 11 ++-- app/views/home/index.html.haml | 17 +++--- config/locales/en.yml | 72 +++++++++++++++++++++++-- 10 files changed, 114 insertions(+), 83 deletions(-) diff --git a/app/views/home/_blurb.html.haml b/app/views/home/_blurb.html.haml index fe06dad9c..3e85b4496 100644 --- a/app/views/home/_blurb.html.haml +++ b/app/views/home/_blurb.html.haml @@ -4,15 +4,13 @@ .row .col-md-8.info - %p - #{ENV['GROWSTUFF_SITE_NAME']} #{t('.intro')} + %p= t('.intro', site_name: ENV['GROWSTUFF_SITE_NAME']) = render :partial => 'stats' .col-md-4.signup %p= t('.perks') - %p= link_to "#{t('.sign_up')}", new_member_registration_path, :class => 'btn btn-primary btn-lg' + %p= link_to(t('.sign_up'), new_member_registration_path, :class => 'btn btn-primary btn-lg') %p %small - = t('.already') - = link_to "#{t('.sign_in')}", new_member_session_path + = t('.already_html', sign_in: link_to(t('.sign_in_linktext'), new_member_session_path)) diff --git a/app/views/home/_crops.html.haml b/app/views/home/_crops.html.haml index 62b6d9e4a..ce3c775ce 100644 --- a/app/views/home/_crops.html.haml +++ b/app/views/home/_crops.html.haml @@ -1,14 +1,14 @@ .row .col-md-6.hidden-xs - cache "interesting_crops", :expires_in => 1.day do - %h2 Some of our crops + %h2= t('.our_crops') - Crop.interesting.each do |c| .col-md-3{:style => 'margin:0px; padding: 3px'} = render :partial => 'crops/image_with_popover', :locals => { :crop => c } .col-md-6 - cache "interesting_plantings" do - %h2 Recently planted + %h2= t('.recently_planted') = render :partial => 'plantings/list', :locals => { :plantings => Planting.interesting.first(4) } .row @@ -16,8 +16,8 @@ - cache "recent_crops" do %p{ :style => 'margin-top: 11.25px' } %strong - Recently added crops: + #{t('.recently_added')}: != Crop.recent.limit(12).map {|c| link_to(c, c) }.join(", ") %p.text-right - =link_to "View all crops »", crops_path + =link_to "#{t('.view_all')} »", crops_path diff --git a/app/views/home/_discuss.html.haml b/app/views/home/_discuss.html.haml index 7da71a54d..22b9972b9 100644 --- a/app/views/home/_discuss.html.haml +++ b/app/views/home/_discuss.html.haml @@ -1,4 +1,4 @@ -%h2 Discussion +%h2= t('.discussion') - cache "recent_posts" do - posts = Post.limit(6) @@ -10,9 +10,9 @@ - if forums %ul.list-inline %li - %strong Forums: + %strong #{t('.forums')}: - forums.each do |f| %li= link_to f.name, f %p.text-right - = link_to "View all posts »", posts_path + = link_to "#{t('.view_all')} »", posts_path diff --git a/app/views/home/_keep_in_touch.html.haml b/app/views/home/_keep_in_touch.html.haml index 34365352b..5bc821887 100644 --- a/app/views/home/_keep_in_touch.html.haml +++ b/app/views/home/_keep_in_touch.html.haml @@ -1,17 +1,14 @@ -%h2 Keep in touch +%h2= t('.keep_in_touch') %p =image_tag("twitter_32.png", :alt => '') - Follow - =link_to '@growstufforg', 'http://twitter.com/growstufforg' - on Twitter. + = t('.twitter_html', link: link_to(t('.twitter_linktext'), 'http://twitter.com/growstufforg')) %p =image_tag("blog_32.png", :alt => '') - Subscribe to the - =link_to 'Growstuff Blog', 'http://blog.growstuff.org/' + = t('.blog_html', link: link_to(t('.blog_linktext'), 'http://blog.growstuff.org/')) %p =image_tag("email_32.png", :alt => '') - Subscribe to our - =link_to 'email newsletter', 'http://blog.growstuff.org/newsletter' + = t('.newsletter_html', link: link_to(t('.newsletter_linktext'), 'http://blog.growstuff.org/newsletter')) + diff --git a/app/views/home/_members.html.haml b/app/views/home/_members.html.haml index 6d3437302..25cb0867b 100644 --- a/app/views/home/_members.html.haml +++ b/app/views/home/_members.html.haml @@ -2,7 +2,7 @@ .hidden-xs - members = Member.interesting.first(6) - if members.present? - %h2 Some of our members + %h2= t('.title') .row - members.each do |m| @@ -10,4 +10,4 @@ = render :partial => "members/thumbnail", :locals => { :member => m } %p.text-right - = link_to "View all members »", members_path + = link_to "#{t('.view_all')} »", members_path diff --git a/app/views/home/_open.html.haml b/app/views/home/_open.html.haml index 0cc44700e..1eeb2d847 100644 --- a/app/views/home/_open.html.haml +++ b/app/views/home/_open.html.haml @@ -1,43 +1,21 @@ -%h2 Open Source +%h2= t('.open_source_title') %p - #{ENV['GROWSTUFF_SITE_NAME']} is open source software, - which means that we share this website's code for free with our - community and the world. We believe that openness, - sustainability, and social good go hand in hand. You can read more - about - =link_to "why Growstuff is open source", "http://blog.growstuff.org/2013/02/20/why-growstuff-is-open-source/" - or check out our code on - =succeed '.' do - = link_to 'Github', 'http://github.com/Growstuff/growstuff' + #{ENV['GROWSTUFF_SITE_NAME']} #{t('.open_source_body_html', why: link_to(t('.why_linktext'), "http://blog.growstuff.org/2013/02/20/why-growstuff-is-open-source/"), github: link_to(t('.github_linktext'), 'http://github.com/Growstuff/growstuff'))} -%h2 Open Data and APIs + +%h2= t('.open_data_title') %p - We're building a database - of crops, planting advice, seed sources, and other information - that anyone can use for free, under a - = succeed '.' do - = link_to 'Creative Commons license', 'http://creativecommons.org/licenses/by-sa/3.0/' - You can use this data for research, to build apps, or for any - purpose at all. Read more about our + = t('.open_data_body_html', creative_commons_link: link_to(t('.creative_commons_linktext'), 'http://creativecommons.org/licenses/by-sa/3.0/'), wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/index.php/Open_data'), api_docs_link: link_to(t('.api_docs_linktext'), 'http://wiki.growstuff.org/index.php/API')) - = link_to 'open data', 'http://wiki.growstuff.org/index.php/Open_data' - and - = succeed '.' do - = link_to 'API documentation', 'http://wiki.growstuff.org/index.php/API' -%h2 Get Involved + +%h2= t('.get_involved_title') %p - We believe in collaboration, and work closely with our members - and the wider food-growing community. - Our team includes volunteers from all walks of life - and all skill levels. To get involved, visit - = link_to 'Growstuff Talk', 'http://talk.growstuff.org/' - or find more information on the - = succeed "." do - = link_to 'Growstuff Wiki', 'http://wiki.growstuff.org/' + = t('.get_involved_body_html', talk_link: link_to(t('.talk_linktext'), 'http://talk.growstuff.org/'), wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/')) + %h2 Support Growstuff diff --git a/app/views/home/_seeds.html.haml b/app/views/home/_seeds.html.haml index ccacd0bf4..52d996373 100644 --- a/app/views/home/_seeds.html.haml +++ b/app/views/home/_seeds.html.haml @@ -1,18 +1,18 @@ - seeds = Seed.interesting.first(6) - if seeds.present? - %h2 Seeds available to trade + %h2= t('.title') - cache "interesting_seeds" do - if seeds.length > 0 %table.table.table-striped %tr - %th Owner - %th Crop - %th.hidden-xs.hidden-sm Description - %th Will trade to - %th From location + %th= t('.owner') + %th= t('.crop') + %th.hidden-xs.hidden-sm= t('.description') + %th= t('.trade_to') + %th= t('.from') %th - seeds.each do |seed| @@ -23,8 +23,8 @@ %td= seed.tradable? ? seed.tradable_to : '' %td - if seed.tradable? - = seed.owner.location.blank? ? "unspecified" : truncate(seed.owner.location, :length => 25, :separator => ', ') - %td= link_to 'Details', seed, :class => 'btn btn-default btn-xs' + = seed.owner.location.blank? ? t('.unspecified') : truncate(seed.owner.location, :length => 25, :separator => ', ') + %td= link_to t('.details'), seed, :class => 'btn btn-default btn-xs' %p.text-right - = link_to "View all seeds »", seeds_path + = link_to "#{t('.view_all')} »", seeds_path diff --git a/app/views/home/_stats.html.haml b/app/views/home/_stats.html.haml index 93c16327d..78ab2c0bc 100644 --- a/app/views/home/_stats.html.haml +++ b/app/views/home/_stats.html.haml @@ -1,10 +1,5 @@ - cache("homepage_stats") do %p.stats - So far, - = link_to "#{Member.confirmed.count.to_i} members", members_path - have planted - = link_to "#{Crop.count.to_i} crops", crops_path - = link_to "#{Planting.count.to_i} times", plantings_path - in - = succeed "." do - = link_to "#{Garden.count.to_i} gardens", gardens_path + = t('.message_html', member: link_to(t('.member_linktext', count: Member.confirmed.count.to_i), members_path), number_crops: link_to(t('.number_crops_linktext', count: Crop.count.to_i), crops_path), number_plantings: link_to(t('.number_plantings_linktext', count: Planting.count.to_i), plantings_path), number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path)) + + diff --git a/app/views/home/index.html.haml b/app/views/home/index.html.haml index 57fb6473f..f36a29aab 100644 --- a/app/views/home/index.html.haml +++ b/app/views/home/index.html.haml @@ -1,19 +1,16 @@ .row .col-md-12 - if member_signed_in? - %h1 - Welcome to - = succeed "," do - = ENV['GROWSTUFF_SITE_NAME'] - = current_member + %h1= t('.welcome', site_name: ENV['GROWSTUFF_SITE_NAME'], member_name: current_member) + = render :partial => 'stats' %p .btn-group - = link_to "Plant", new_planting_path, :class => 'btn btn-default' - = link_to "Harvest", new_harvest_path, :class => 'btn btn-default' - = link_to "Add seeds", new_seed_path, :class => 'btn btn-default' - = link_to "Post", new_post_path, :class => 'btn btn-default' - = link_to "Edit profile", edit_member_registration_path, :class => 'btn btn-default' + = link_to t('.plant'), new_planting_path, :class => 'btn btn-default' + = link_to t('.harvest'), new_harvest_path, :class => 'btn btn-default' + = link_to t('.add_seeds'), new_seed_path, :class => 'btn btn-default' + = link_to t('.post'), new_post_path, :class => 'btn btn-default' + = link_to t('.edit_profile'), edit_member_registration_path, :class => 'btn btn-default' - else .hidden-xs diff --git a/config/locales/en.yml b/config/locales/en.yml index 591ddd0e8..9b650e582 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -16,8 +16,74 @@ en: home: blurb: - intro: "is a community of food gardeners. We're building an open source platform to help you learn about growing food, track what you plant and harvest, and swap seeds and produce with other gardeners near you." + intro: "%{site_name} is a community of food gardeners. We're building an open source platform to help you learn about growing food, track what you plant and harvest, and swap seeds and produce with other gardeners near you." perks: "Join now for your free garden journal, seed sharing, forums, and more." sign_up: "Sign up" - sign_in: "Sign in" - already: "Already have an account?" \ No newline at end of file + already_html: "Or %{sign_in} if you already have an account" + sign_in_linktext: "sign in" + + crops: + our_crops: "Some of our crops" + recently_planted: "Recently planted" + recently_added: "Recently added crops" + view_all: "View all crops" + + discuss: + discussion: "Discussion" + forums: "Forums" + view_all: "View all posts" + + keep_in_touch: + keep_in_touch: "Keep in touch" + twitter_html: "Follow %{link} on Twitter" + twitter_linktext: "@growstufforg" + blog_html: "Subscribe to the %{link}" + blog_linktext: "Growstuff Blog" + newsletter_html: "Subscribe to our %{link}" + newsletter_linktext: "email newsletter" + + members: + title: "Some of our members" + view_all: "View all members" + + open: + open_source_title: "Open Source" + open_source_body_html: "is open source software, which means that we share this website's code for free with our community and the world. We believe that openness, sustainability, and social good go hand in hand. You can read more about %{why} or check out our code on %{github}." + why_linktext: "why Growstuff is open source" + github_linktext: "Github" + open_data_title: "Open Data and APIs" + open_data_body_html: "We're building a database of crops, planting advice, seed sources, and other information that anyone can use for free, under a %{creative_commons_link}. You can use this data for research, to build apps, or for any purpose at all. Read more about our %{wiki_link} and %{api_docs_link}." + creative_commons_linktext: "Creative Commons license" + wiki_linktext: "open data" + api_docs_linktext: "API documentation" + get_involved_title: "Get Involved" + get_involved_body_html: "We believe in collaboration, and work closely with our members and the wider food-growing community. Our team includes volunteers from all walks of life and all skill levels. To get involved, visit %{talk_link} or find more information on the %{wiki_link}." + talk_linktext: "Growstuff Talk" + wiki_linktext: "Growstuff Wiki" + + seeds: + title: "Seeds available to trade" + owner: "Owner" + crop: "Crop" + description: "Description" + trade_to: "Will trade to" + from: "From location" + unspecified: "unspecified" + details: "Details" + view_all: "View all seeds" + + stats: + message_html: "So far, %{member} have planted %{number_crops} %{number_plantings} in %{number_gardens}." + member_linktext: "%{count} members" + number_crops_linktext: "%{count} crops" + number_plantings_linktext: "%{count} times" + number_gardens_linktext: "%{count} gardens" + + index: + welcome: "Welcome to %{site_name}, %{member_name}" + plant: "Plant" + harvest: "Harvest" + add_seeds: "Add seeds" + post: "Post" + edit_profile: "Edit profile" + From 659a68cf3971b6894f7f557d7f00921722c98a6a Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sat, 20 Sep 2014 21:49:30 +1000 Subject: [PATCH 021/132] remove missing translations spec in favor of running dedicated task --- spec/i18n_spec.rb | 18 ------------------ 1 file changed, 18 deletions(-) delete mode 100644 spec/i18n_spec.rb diff --git a/spec/i18n_spec.rb b/spec/i18n_spec.rb deleted file mode 100644 index 0475c9fcc..000000000 --- a/spec/i18n_spec.rb +++ /dev/null @@ -1,18 +0,0 @@ -require 'spec_helper' -require 'i18n/tasks' - -describe 'I18n' do - let(:i18n) { I18n::Tasks::BaseTask.new } - let(:missing_keys) { i18n.missing_keys } - let(:unused_keys) { i18n.unused_keys } - - it 'does not have missing keys' do - expect(missing_keys).to be_empty, - "Missing #{missing_keys.leaves.count} i18n keys, run `i18n-tasks missing' to show them" - end - - it 'does not have unused keys' do - expect(i18n.unused_keys).to be_empty, - "#{unused_keys.leaves.count} unused i18n keys, run `i18n-tasks unused' to show them" - end -end From e3a6742574dde7b905356203a1c23ef7623f0fae Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sat, 20 Sep 2014 21:56:14 +1000 Subject: [PATCH 022/132] add sample japanese locale file to test changing languages --- config/locales/ja.yml | 74 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 73 insertions(+), 1 deletion(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index c213277b9..6c7d63b8f 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -1,2 +1,74 @@ ja: - blurb: "はガーデナーのコミュニティーです。" + home: + blurb: + intro: "%{site_name}はガーデナーのコミュニティです。" + perks: "翻訳中" + sign_up: "翻訳中" + already_html: "翻訳中" + sign_in_linktext: "翻訳中" + + crops: + our_crops: "翻訳中" + recently_planted: "翻訳中" + recently_added: "翻訳中" + view_all: "翻訳中" + + discuss: + discussion: "翻訳中" + forums: "翻訳中" + view_all: "翻訳中" + + keep_in_touch: + keep_in_touch: "翻訳中" + twitter_html: "翻訳中" + twitter_linktext: "翻訳中" + blog_html: "翻訳中" + blog_linktext: "翻訳中" + newsletter_html: "翻訳中" + newsletter_linktext: "翻訳中" + + members: + title: "翻訳中" + view_all: "翻訳中" + + open: + open_source_title: "翻訳中" + open_source_body_html: "翻訳中" + why_linktext: "翻訳中" + github_linktext: "翻訳中" + open_data_title: "翻訳中" + open_data_body_html: "翻訳中" + creative_commons_linktext: "翻訳中" + wiki_linktext: "翻訳中" + api_docs_linktext: "翻訳中" + get_involved_title: "翻訳中" + get_involved_body_html: "翻訳中" + talk_linktext: "翻訳中" + wiki_linktext: "翻訳中" + + seeds: + title: "翻訳中" + owner: "翻訳中" + crop: "翻訳中" + description: "翻訳中" + trade_to: "翻訳中" + from: "翻訳中" + unspecified: "翻訳中" + details: "翻訳中" + view_all: "翻訳中" + + stats: + message_html: "翻訳中" + member_linktext: "翻訳中" + number_crops_linktext: "翻訳中" + number_plantings_linktext: "翻訳中" + number_gardens_linktext: "翻訳中" + + index: + welcome: "翻訳中" + plant: "翻訳中" + harvest: "翻訳中" + add_seeds: "翻訳中" + post: "翻訳中" + edit_profile: "翻訳中" + From 93cd067fff99086cfb7d4d99ce9b3f67e2fa770a Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sat, 20 Sep 2014 21:59:05 +1000 Subject: [PATCH 023/132] add two contributors --- CONTRIBUTORS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 5bd5cc601..8473e9f2a 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -45,3 +45,5 @@ submit the change with your pull request. - Marlena Compton / [Marlena](https://github.com/marlena) - Elizabeth A. Kari / [catfriend](https://github.com/catfriend) - Cheri Allen / [cherimarie](https://github.com/cherimarie) +- Shiho Takagi / [oshiho3](https:://github.com/oshiho3) +- Maki Sugita / [macckii](https:://github.com/macckii) From 1acba65a8b01f74292eca50ac038e2701fa6a085 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 21 Sep 2014 10:22:46 +1000 Subject: [PATCH 024/132] clean up haml a bit --- app/views/home/_open.html.haml | 11 ++++++++--- app/views/home/_stats.html.haml | 5 ++++- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/app/views/home/_open.html.haml b/app/views/home/_open.html.haml index 1eeb2d847..f53c32675 100644 --- a/app/views/home/_open.html.haml +++ b/app/views/home/_open.html.haml @@ -1,20 +1,25 @@ %h2= t('.open_source_title') %p - #{ENV['GROWSTUFF_SITE_NAME']} #{t('.open_source_body_html', why: link_to(t('.why_linktext'), "http://blog.growstuff.org/2013/02/20/why-growstuff-is-open-source/"), github: link_to(t('.github_linktext'), 'http://github.com/Growstuff/growstuff'))} + = ENV['GROWSTUFF_SITE_NAME'] + = t('.open_source_body_html', why: link_to(t('.why_linktext'), 'http://blog.growstuff.org/2013/02/20/why-growstuff-is-open-source/'), + github: link_to(t('.github_linktext'), 'http://github.com/Growstuff/growstuff') ) %h2= t('.open_data_title') %p - = t('.open_data_body_html', creative_commons_link: link_to(t('.creative_commons_linktext'), 'http://creativecommons.org/licenses/by-sa/3.0/'), wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/index.php/Open_data'), api_docs_link: link_to(t('.api_docs_linktext'), 'http://wiki.growstuff.org/index.php/API')) + = t('.open_data_body_html', creative_commons_link: link_to(t('.creative_commons_linktext'), 'http://creativecommons.org/licenses/by-sa/3.0/'), + wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/index.php/Open_data'), + api_docs_link: link_to(t('.api_docs_linktext'), 'http://wiki.growstuff.org/index.php/API')) %h2= t('.get_involved_title') %p - = t('.get_involved_body_html', talk_link: link_to(t('.talk_linktext'), 'http://talk.growstuff.org/'), wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/')) + = t('.get_involved_body_html', talk_link: link_to(t('.talk_linktext'), 'http://talk.growstuff.org/'), + wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/')) %h2 Support Growstuff diff --git a/app/views/home/_stats.html.haml b/app/views/home/_stats.html.haml index 78ab2c0bc..e13f74719 100644 --- a/app/views/home/_stats.html.haml +++ b/app/views/home/_stats.html.haml @@ -1,5 +1,8 @@ - cache("homepage_stats") do %p.stats - = t('.message_html', member: link_to(t('.member_linktext', count: Member.confirmed.count.to_i), members_path), number_crops: link_to(t('.number_crops_linktext', count: Crop.count.to_i), crops_path), number_plantings: link_to(t('.number_plantings_linktext', count: Planting.count.to_i), plantings_path), number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path)) + = t('.message_html', { member: link_to(t('.member_linktext', count: Member.confirmed.count.to_i), members_path), + number_crops: link_to(t('.number_crops_linktext', count: Crop.count.to_i), crops_path), + number_plantings: link_to(t('.number_plantings_linktext', count: Planting.count.to_i), plantings_path), + number_gardens: link_to(t('.number_gardens_linktext', count: Garden.count.to_i), gardens_path) }) From d4622540219da913ab90fd95fd78f22c3d36fc35 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 21 Sep 2014 10:26:30 +1000 Subject: [PATCH 025/132] add missing translation --- app/views/home/_open.html.haml | 10 +++------- config/locales/en.yml | 4 ++++ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/app/views/home/_open.html.haml b/app/views/home/_open.html.haml index f53c32675..b14bfa83b 100644 --- a/app/views/home/_open.html.haml +++ b/app/views/home/_open.html.haml @@ -22,12 +22,8 @@ wiki_link: link_to(t('.wiki_linktext'), 'http://wiki.growstuff.org/')) -%h2 Support Growstuff +%h2= t('.support_title') %p - Growstuff is independent, - =succeed "," do - =link_to 'ad-free', 'http://wiki.growstuff.org/index.php/Why_no_ads%3F' - and we have no outside investment. You can support our work by - =succeed "." do - =link_to 'buying a paid account', shop_path + = t('.support_body_html', ad_free: link_to(t('.ad_free_linktext'), 'http://wiki.growstuff.org/index.php/Why_no_ads%3F'), + buy_account: link_to(t('.buy_account_linktext'), shop_path)) diff --git a/config/locales/en.yml b/config/locales/en.yml index 9b650e582..b32bd931f 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -60,6 +60,10 @@ en: get_involved_body_html: "We believe in collaboration, and work closely with our members and the wider food-growing community. Our team includes volunteers from all walks of life and all skill levels. To get involved, visit %{talk_link} or find more information on the %{wiki_link}." talk_linktext: "Growstuff Talk" wiki_linktext: "Growstuff Wiki" + support_title: "Support Growstuff" + support_body_html: "Growstuff is independent, %{ad_free} and we have no outside investment. You can support our work by %{buy_account}." + ad_free_linktext: "ad-free" + buy_account_linktext: "buying a paid account" seeds: title: "Seeds available to trade" From 5e48c4392ab80040d53cf941cf7b5f7568fef2a7 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Wed, 24 Sep 2014 18:29:59 +1000 Subject: [PATCH 026/132] move site name into localization --- app/views/home/_open.html.haml | 4 ++-- config/locales/en.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/home/_open.html.haml b/app/views/home/_open.html.haml index b14bfa83b..fa7c00f57 100644 --- a/app/views/home/_open.html.haml +++ b/app/views/home/_open.html.haml @@ -1,9 +1,9 @@ %h2= t('.open_source_title') %p - = ENV['GROWSTUFF_SITE_NAME'] = t('.open_source_body_html', why: link_to(t('.why_linktext'), 'http://blog.growstuff.org/2013/02/20/why-growstuff-is-open-source/'), - github: link_to(t('.github_linktext'), 'http://github.com/Growstuff/growstuff') ) + github: link_to(t('.github_linktext'), 'http://github.com/Growstuff/growstuff'), + site_name: ENV['GROWSTUFF_SITE_NAME'] ) %h2= t('.open_data_title') diff --git a/config/locales/en.yml b/config/locales/en.yml index b32bd931f..39bb974ff 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -48,7 +48,7 @@ en: open: open_source_title: "Open Source" - open_source_body_html: "is open source software, which means that we share this website's code for free with our community and the world. We believe that openness, sustainability, and social good go hand in hand. You can read more about %{why} or check out our code on %{github}." + open_source_body_html: "%{site_name} is open source software, which means that we share this website's code for free with our community and the world. We believe that openness, sustainability, and social good go hand in hand. You can read more about %{why} or check out our code on %{github}." why_linktext: "why Growstuff is open source" github_linktext: "Github" open_data_title: "Open Data and APIs" From 984503480f074fcdb9218cd9458efec983845878 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 25 Sep 2014 07:00:11 +1000 Subject: [PATCH 027/132] remove necessity to add locale to url if current locale is English --- app/controllers/application_controller.rb | 6 +++++- spec/features/crop_wranglers_spec.rb | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 91fc81413..8fbc8ba95 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -40,7 +40,11 @@ class ApplicationController < ActionController::Base # Adds locale query parameter to every path / url helper def default_url_options(options={}) - { locale: I18n.locale } + if I18n.locale == :en + {} + else + { locale: I18n.locale } + end end end diff --git a/spec/features/crop_wranglers_spec.rb b/spec/features/crop_wranglers_spec.rb index 327be7e51..80475bd18 100644 --- a/spec/features/crop_wranglers_spec.rb +++ b/spec/features/crop_wranglers_spec.rb @@ -19,7 +19,7 @@ feature "crop wranglers" do within '.crop_wranglers' do expect(page).to have_content 'Crop Wranglers:' crop_wranglers.each do |crop_wrangler| - page.should have_link crop_wrangler.login_name, :href => member_path(crop_wrangler, {locale: 'en'}) + page.should have_link crop_wrangler.login_name, :href => member_path(crop_wrangler) end end end From 8ab4f885f11db1ec53fb568b728b2abc49b0c79d Mon Sep 17 00:00:00 2001 From: Wendy Smoak Date: Fri, 26 Sep 2014 10:32:06 -0400 Subject: [PATCH 028/132] Renamed gardens.rb to gardens_spec.rb so that these tests will run. --- spec/features/{gardens.rb => gardens_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/features/{gardens.rb => gardens_spec.rb} (100%) diff --git a/spec/features/gardens.rb b/spec/features/gardens_spec.rb similarity index 100% rename from spec/features/gardens.rb rename to spec/features/gardens_spec.rb From 25abcdc9236aea1efaaad8683b0f246faffe91f8 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 13:50:55 +1000 Subject: [PATCH 029/132] Upload tomato varieties to crop database --- db/seeds/README.md | 6 +++++ db/seeds/crops-11-tomatoes.csv | 41 ++++++++++++++++++++++++++++++++++ script/deploy-tasks.sh | 7 ++---- 3 files changed, 49 insertions(+), 5 deletions(-) create mode 100644 db/seeds/README.md create mode 100644 db/seeds/crops-11-tomatoes.csv diff --git a/db/seeds/README.md b/db/seeds/README.md new file mode 100644 index 000000000..c3cfd1902 --- /dev/null +++ b/db/seeds/README.md @@ -0,0 +1,6 @@ +The format for these crop seed files is CSV with the following fields: + +Crop name +Scientific name +English Wikipedia URL +Parent crop name diff --git a/db/seeds/crops-11-tomatoes.csv b/db/seeds/crops-11-tomatoes.csv new file mode 100644 index 000000000..eb28aaa79 --- /dev/null +++ b/db/seeds/crops-11-tomatoes.csv @@ -0,0 +1,41 @@ +adoration tomato,,https://en.wikipedia.org/wiki/Adoration_%28Tomato%29,tomato, +alicante tomato,,https://en.wikipedia.org/wiki/Alicante_%28tomato%29,tomato, +Amish paste tomato,,http://en.wikipedia.org/wiki/Amish_Paste,tomato, +azoychka tomato,,https://en.wikipedia.org/wiki/Azoychka%28Tomato%29,tomato, +beefsteak tomato,,https://en.wikipedia.org/wiki/Beefsteak_(tomato),tomato, +better boy tomato,,https://en.wikipedia.org/wiki/Better_Boy,tomato, +big rainbow tomato,,https://en.wikipedia.org/wiki/Big_Rainbow_(Tomato),tomato, +Blaby special tomato,,https://en.wikipedia.org/wiki/Blaby_Special_(Tomato),tomato, +black krim tomato,,https://en.wikipedia.org/wiki/Black_Krim_%28tomato%29,tomato, +brandywine tomato,,https://en.wikipedia.org/wiki/Brandywine_(tomato),tomato, +campari tomato,,https://en.wikipedia.org/wiki/Campari_tomato,tomato, +Cherokee purple tomato,,https://en.wikipedia.org/wiki/Cherokee_purple,tomato, +currant tomato,solanum pimpinellifolium,https://en.wikipedia.org/wiki/Solanum_pimpinellifolium, +early girl tomato,,https://en.wikipedia.org/wiki/Early_Girl,tomato, +Fourth of July tomato,,https://en.wikipedia.org/wiki/Fourth_of_July_(tomato_variety),tomato, +garden peach tomato,,https://en.wikipedia.org/wiki/Garden_peach_tomato,tomato, +green zebra tomato,,https://en.wikipedia.org/wiki/Green_Zebra,tomato, +hillbilly tomato,,http://en.wikipedia.org/wiki/Hillbilly_(tomato),tomato, +jubilee tomato,,http://en.wikipedia.org/wiki/Jubilee_(tomato),tomato, +lillian's yellow tomato,,http://en.wikipedia.org/wiki/Lillian%27s_Yellow_(tomato),tomato, +Matt's wild cherry tomato,,http://en.wikipedia.org/wiki/Matt%27s_Wild_Cherry,tomato, +mortgage lifter tomato,,http://en.wikipedia.org/wiki/Mortgage_Lifter,tomato, +Mr. Stripey tomato,,http://en.wikipedia.org/wiki/Mr._Stripey,tomato, +Roma tomato,,http://en.wikipedia.org/wiki/Roma_tomato,tomato, +San Marzano tomato,,http://en.wikipedia.org/wiki/San_Marzano_tomato,tomato, +Santorini tomato,,http://en.wikipedia.org/wiki/Santorini_(tomato),tomato, +stupice tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,tomato, +tigerella tomato,,http://en.wikipedia.org/wiki/Tigerella,tomato, +tomaccio tomato,,http://en.wikipedia.org/wiki/Tomaccio_(tomato),tomato, +traveller tomato,,http://en.wikipedia.org/wiki/Traveller_(tomato),tomato +three sisters tomato,,http://en.wikipedia.org/wiki/Three_Sisters_(tomato),tomato, +Hanover tomato,,http://en.wikipedia.org/wiki/Hanover_tomato,tomato, +celebrity tomato,,http://en.wikipedia.org/wiki/Celebrity_(tomato),tomato, +tomberry,,http://en.wikipedia.org/wiki/Tomberry,tomato, +super sweet 100 tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,tomato, +marglobe tomato,,http://en.wikipedia.org/wiki/Marglobe,tomato, +grape tomato,,http://en.wikipedia.org/wiki/Grape_tomato,tomato, +cherry tomato,,http://en.wikipedia.org/wiki/Cherry_tomato,tomato, +Aunt Ruby's German green tomato,,http://en.wikipedia.org/wiki/Aunt_Ruby%27s_German_Green,tomato, +white queen tomato,,http://en.wikipedia.org/wiki/White_Queen_tomato,tomato, +pear tomato,,http://en.wikipedia.org/wiki/Pear_tomato,tomato, diff --git a/script/deploy-tasks.sh b/script/deploy-tasks.sh index f861d27ed..ac4068b26 100755 --- a/script/deploy-tasks.sh +++ b/script/deploy-tasks.sh @@ -10,8 +10,5 @@ # echo "YYYY-MM-DD - do something or other" # rake growstuff:oneoff:something -echo "2013-07-18 - zero crop plantings_count" -rake growstuff:oneoff:zero_plantings_count - -echo "2014-08-10 - replace ping with pint in db" -rake growstuff:oneoff:ping_to_pint +echo "2014-09-28 - upload tomatoes" +rake growstuff:import_crops file=db/seeds/crops-11-tomatoes.csv From 1b1f1d33e01cfd85d9519b63728697691a547d0c Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 14:35:32 +1000 Subject: [PATCH 030/132] Add planting reminder checkbox to email settings --- app/views/devise/registrations/_edit_email.html.haml | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app/views/devise/registrations/_edit_email.html.haml b/app/views/devise/registrations/_edit_email.html.haml index 61df13f91..252263703 100644 --- a/app/views/devise/registrations/_edit_email.html.haml +++ b/app/views/devise/registrations/_edit_email.html.haml @@ -10,12 +10,17 @@ .form-group .col-md-offset-2.col-md-8 = f.check_box :show_email - Show email publicly on your profile page + Show email publicly on your profile page. .form-group .col-md-offset-2.col-md-8 = f.check_box :send_notification_email - Receive emailed copies of Inbox notifications. + Receive emailed copies of Inbox notifications (eg. private messages). + + .form-group + .col-md-offset-2.col-md-8 + = f.check_box :send_planting_reminder + Receive regular reminders to track your planting and harvesting. .form-group .col-md-offset-2.col-md-8 From e011b54cbac18b6744d16bf197c75d60e35f6b97 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 14:35:54 +1000 Subject: [PATCH 031/132] Nicer phrasing for the no-plantings use case --- .../notifier/planting_reminder.html.haml | 29 ++++++++++++------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/app/views/notifier/planting_reminder.html.haml b/app/views/notifier/planting_reminder.html.haml index 98ca52fa9..eb7489184 100644 --- a/app/views/notifier/planting_reminder.html.haml +++ b/app/views/notifier/planting_reminder.html.haml @@ -4,14 +4,14 @@ %h2 What's new in your garden? %p - Have you planted anything recently? - if @member.plantings.size == 0 %p #{ENV['GROWSTUFF_SITE_NAME']} lets you track what food you're growing - in your garden. + in your garden and see what other people near you are planting too. %p - = link_to "Get started now.", new_planting_url + = link_to "Get started now", new_planting_url + by planting your first crop. - else %p @@ -26,18 +26,20 @@ ago. %p - Planted anything new? - = link_to "Track your plantings here.", new_planting_url + = link_to "Plant something new", new_planting_url + to keep your garden records up to date. %h2 Your recent harvests - if @member.harvests.size == 0 %p - With #{ENV['GROWSTUFF_SITE_NAME']} you can keep track of what you - harvest from your garden. + #{ENV['GROWSTUFF_SITE_NAME']} helps you keep track of what you + harvest from your garden. Record what food you've grown + and see what other people near you are harvesting, too. %p - = link_to "Get started now.", new_harvest_url + = link_to "Get started now", new_harvest_url + by tracking your first harvest. - else According to our records, the last few things you harvested were: @@ -54,11 +56,16 @@ Harvested anything else lately? = link_to "Track your harvests here.", new_harvest_url -%p -Don't want to get these emails any more? -= link_to "Turn off these notifications", edit_member_registration_url +%h2 + See you soon on #{ENV['GROWSTUFF_SITE_NAME']}! %p The #{site_name} team. %br/ =link_to root_url, root_url + +%hr/ +%p + Don't want to get these emails any more? + = link_to "Turn off these notifications", edit_member_registration_url + From 529c98f5dc6120225dde51ebf47f66b8da530bc9 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 15:28:53 +1000 Subject: [PATCH 032/132] Move actual sending of planting reminder to rake task --- app/controllers/members_controller.rb | 3 --- app/models/ability.rb | 1 - config/routes.rb | 1 - lib/tasks/growstuff.rake | 20 ++++++++++++++++++++ 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index f1e00a37a..fdc297740 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -32,8 +32,5 @@ class MembersController < ApplicationController end end - def send_planting_reminder - Notifier.planting_reminder(current_member).deliver! - end end diff --git a/app/models/ability.rb b/app/models/ability.rb index 8e4e30fe6..e33f4a022 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -23,7 +23,6 @@ class Ability # managing your own user settings can :update, Member, :id => member.id - can :send_planting_reminder, Member # can read/delete notifications that were sent to them can :read, Notification, :recipient_id => member.id diff --git a/config/routes.rb b/config/routes.rb index 4bc93c2fb..00e31fec8 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -2,7 +2,6 @@ Growstuff::Application.routes.draw do resources :plant_parts - get '/members/send_planting_reminder' => 'members#send_planting_reminder', :as => 'send_planting_reminder' devise_for :members, :controllers => { :registrations => "registrations", :passwords => "passwords" } resources :members diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 34739217b..8ae3b8659 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -34,6 +34,26 @@ namespace :growstuff do end + desc "Send planting reminder email" + # usage: rake growstuff:send_planting_reminder + + task :send_planting_reminder => :environment do + # Heroku scheduler only lets us run things daily, so this checks + # whether it's the right day to actually do the deed. + # Note that Heroku scheduler runs on UTC. + # We'd like to send on Wednesday mornings, US time, which will be + # very early Wednesday morning UTC. + send_on_day = 3 # wednesday + every_n_weeks = 2 # send fortnightly + + if Date.today.cwday == send_on_day and Date.today.cw_week % every_n_weeks == 0 + puts "We're going to send email because it's the right day" + Member.find_each do |m| + Notifier.planting_reminder(m).deliver! + end + end + end + desc "Depopulate Null Island" # this fixes up anyone who has erroneously wound up with a 0,0 lat/long task :depopulate_null_island => :environment do From d8b75eaa66d0f29e5c5d7f56bab8d47d5b8d8e94 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 15:55:55 +1000 Subject: [PATCH 033/132] Fixed up db migration and tests for planting reminders --- ...0140928085713_add_send_planting_reminder_to_member.rb} | 0 db/schema.rb | 3 ++- spec/features/planting_reminder_spec.rb | 8 +------- 3 files changed, 3 insertions(+), 8 deletions(-) rename db/migrate/{20140720085713_add_send_planting_reminder_to_member.rb => 20140928085713_add_send_planting_reminder_to_member.rb} (100%) diff --git a/db/migrate/20140720085713_add_send_planting_reminder_to_member.rb b/db/migrate/20140928085713_add_send_planting_reminder_to_member.rb similarity index 100% rename from db/migrate/20140720085713_add_send_planting_reminder_to_member.rb rename to db/migrate/20140928085713_add_send_planting_reminder_to_member.rb diff --git a/db/schema.rb b/db/schema.rb index 29271f5e6..839017c46 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140829230600) do +ActiveRecord::Schema.define(:version => 20140928085713) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -139,6 +139,7 @@ ActiveRecord::Schema.define(:version => 20140829230600) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" + t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb index 763331442..9979ddd99 100644 --- a/spec/features/planting_reminder_spec.rb +++ b/spec/features/planting_reminder_spec.rb @@ -3,18 +3,12 @@ require 'spec_helper' feature "planting reminder" do before :each do @member = FactoryGirl.create(:member) - visit root_path - click_link 'Sign in' - page.should have_content "Sign in" - fill_in 'Login', :with => @member.login_name - fill_in 'Password', :with => @member.password - click_button 'Sign in' end scenario "sends email" do expect { # stub for while we're working on this. remove! - visit send_planting_reminder_path + Notifier.planting_reminder(@member).deliver! }.to change { ActionMailer::Base.deliveries.count }.by(1) end end From 05400a081fa008a1e6c62c01db9d2adeb36ebc89 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 16:02:02 +1000 Subject: [PATCH 034/132] tiny bit of whitespace cleanup --- app/controllers/members_controller.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index fdc297740..ab3de0001 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -32,5 +32,4 @@ class MembersController < ApplicationController end end - end From baeb5d352586f83c3ddd18c425dbe5fe3e75591d Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 28 Sep 2014 16:03:58 +1000 Subject: [PATCH 035/132] Remove old planting reminder sending stuff --- app/views/members/send_planting_reminder.html.haml | 12 ------------ 1 file changed, 12 deletions(-) delete mode 100644 app/views/members/send_planting_reminder.html.haml diff --git a/app/views/members/send_planting_reminder.html.haml b/app/views/members/send_planting_reminder.html.haml deleted file mode 100644 index 1be4469e7..000000000 --- a/app/views/members/send_planting_reminder.html.haml +++ /dev/null @@ -1,12 +0,0 @@ -- content_for :title, "Planting reminder email (beta)" - -- if current_member.send_planting_reminder - %p - We just sent you a planting reminder email. -- else - %p - Your profile settings say not to send you this email, so we haven't. - -%p - = link_to "Back to your profile", edit_member_registration_path - From d10a62d336405c88418247ec56f960becd346764 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 28 Sep 2014 21:32:15 +1000 Subject: [PATCH 036/132] set locale from subdomain --- app/controllers/application_controller.rb | 21 +++++++++++++-------- config/locales/ja.yml | 4 ++++ spec/features/signup_spec.rb | 13 +++++++++++++ 3 files changed, 30 insertions(+), 8 deletions(-) create mode 100644 spec/features/signup_spec.rb diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 8fbc8ba95..56db2c730 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -35,16 +35,21 @@ class ApplicationController < ActionController::Base end def set_locale - I18n.locale = params[:locale] || I18n.default_locale + I18n.locale = extract_locale_tld || I18n.default_locale + end + + def extract_locale_tld + parsed_locale = request.subdomains.first + I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end # Adds locale query parameter to every path / url helper - def default_url_options(options={}) - if I18n.locale == :en - {} - else - { locale: I18n.locale } - end - end + # def default_url_options(options={}) + # if I18n.locale == :en + # {} + # else + # { locale: I18n.locale } + # end + # end end diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 6c7d63b8f..6ca6eaf88 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -45,6 +45,10 @@ ja: get_involved_body_html: "翻訳中" talk_linktext: "翻訳中" wiki_linktext: "翻訳中" + support_title: "翻訳中" + support_body_html: "翻訳中" + ad_free_linktext: "翻訳中" + buy_account_linktext: "翻訳中" seeds: title: "翻訳中" diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb new file mode 100644 index 000000000..d3be7e17f --- /dev/null +++ b/spec/features/signup_spec.rb @@ -0,0 +1,13 @@ +require 'spec_helper' + +feature "signup" do + + scenario "signup" do + visit root_path + first('.signup a').click + + expect(current_path).to eq(new_member_registration_path) + end + + +end From f9dc17c5875633594079fd6d4bea64521e65463f Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 28 Sep 2014 21:34:26 +1000 Subject: [PATCH 037/132] no longer raise exception on missing g translation in dev / test envs --- config/initializers/locale.rb | 5 ----- config/locales/ja.yml | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb index 21c2733cb..fb251b800 100644 --- a/config/initializers/locale.rb +++ b/config/initializers/locale.rb @@ -1,9 +1,4 @@ I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] I18n.default_locale = :en -if Rails.env.development? || Rails.env.test? - I18n.exception_handler = lambda do |exception, locale, key, options| - raise "Missing translation: #{key}" - end -end \ No newline at end of file diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 6ca6eaf88..1f2fe3247 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -48,7 +48,7 @@ ja: support_title: "翻訳中" support_body_html: "翻訳中" ad_free_linktext: "翻訳中" - buy_account_linktext: "翻訳中" + seeds: title: "翻訳中" From a7da4b9ce41ac5e1ccec0723d456085778e8d0f5 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 28 Sep 2014 21:38:28 +1000 Subject: [PATCH 038/132] no longer necessary to pass locale quuery param in every url --- app/controllers/application_controller.rb | 9 --------- 1 file changed, 9 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 56db2c730..1b27f01fc 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -43,13 +43,4 @@ class ApplicationController < ActionController::Base I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end - # Adds locale query parameter to every path / url helper - # def default_url_options(options={}) - # if I18n.locale == :en - # {} - # else - # { locale: I18n.locale } - # end - # end - end From 87b6780220071860be7eef24f677fe971f32bfa0 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 28 Sep 2014 21:57:05 +1000 Subject: [PATCH 039/132] stop generating helpers, constroller specs, view specs, and javascripts --- app/assets/javascripts/admin.js.coffee | 3 --- app/assets/javascripts/admin/orders.js.coffee | 3 --- app/assets/javascripts/comments.js.coffee | 3 --- app/assets/javascripts/forums.js.coffee | 3 --- app/assets/javascripts/gardens.js.coffee | 3 --- app/assets/javascripts/harvests.js.coffee | 3 --- app/assets/javascripts/home.js.coffee | 3 --- app/assets/javascripts/notifications.js.coffee | 3 --- app/assets/javascripts/order_items.js.coffee | 3 --- app/assets/javascripts/orders.js.coffee | 3 --- app/assets/javascripts/photos.js.coffee | 3 --- app/assets/javascripts/plant_parts.js.coffee | 3 --- app/assets/javascripts/products.js.coffee | 3 --- app/assets/javascripts/roles.js.coffee | 3 --- app/assets/javascripts/scientific_names.js.coffee | 3 --- app/assets/javascripts/updates.js.coffee | 3 --- app/helpers/plant_parts_helper.rb | 2 -- app/helpers/seeds_helper.rb | 2 -- config/application.rb | 8 ++++++-- 19 files changed, 6 insertions(+), 54 deletions(-) delete mode 100644 app/assets/javascripts/admin.js.coffee delete mode 100644 app/assets/javascripts/admin/orders.js.coffee delete mode 100644 app/assets/javascripts/comments.js.coffee delete mode 100644 app/assets/javascripts/forums.js.coffee delete mode 100644 app/assets/javascripts/gardens.js.coffee delete mode 100644 app/assets/javascripts/harvests.js.coffee delete mode 100644 app/assets/javascripts/home.js.coffee delete mode 100644 app/assets/javascripts/notifications.js.coffee delete mode 100644 app/assets/javascripts/order_items.js.coffee delete mode 100644 app/assets/javascripts/orders.js.coffee delete mode 100644 app/assets/javascripts/photos.js.coffee delete mode 100644 app/assets/javascripts/plant_parts.js.coffee delete mode 100644 app/assets/javascripts/products.js.coffee delete mode 100644 app/assets/javascripts/roles.js.coffee delete mode 100644 app/assets/javascripts/scientific_names.js.coffee delete mode 100644 app/assets/javascripts/updates.js.coffee delete mode 100644 app/helpers/plant_parts_helper.rb delete mode 100644 app/helpers/seeds_helper.rb diff --git a/app/assets/javascripts/admin.js.coffee b/app/assets/javascripts/admin.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/admin.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/admin/orders.js.coffee b/app/assets/javascripts/admin/orders.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/admin/orders.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/comments.js.coffee b/app/assets/javascripts/comments.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/comments.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/forums.js.coffee b/app/assets/javascripts/forums.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/forums.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/gardens.js.coffee b/app/assets/javascripts/gardens.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/gardens.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/harvests.js.coffee b/app/assets/javascripts/harvests.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/harvests.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/home.js.coffee b/app/assets/javascripts/home.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/home.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/notifications.js.coffee b/app/assets/javascripts/notifications.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/notifications.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/order_items.js.coffee b/app/assets/javascripts/order_items.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/order_items.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/orders.js.coffee b/app/assets/javascripts/orders.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/orders.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/photos.js.coffee b/app/assets/javascripts/photos.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/photos.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/plant_parts.js.coffee b/app/assets/javascripts/plant_parts.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/plant_parts.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/products.js.coffee b/app/assets/javascripts/products.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/products.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/roles.js.coffee b/app/assets/javascripts/roles.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/roles.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/scientific_names.js.coffee b/app/assets/javascripts/scientific_names.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/scientific_names.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/assets/javascripts/updates.js.coffee b/app/assets/javascripts/updates.js.coffee deleted file mode 100644 index 761567942..000000000 --- a/app/assets/javascripts/updates.js.coffee +++ /dev/null @@ -1,3 +0,0 @@ -# Place all the behaviors and hooks related to the matching controller here. -# All this logic will automatically be available in application.js. -# You can use CoffeeScript in this file: http://jashkenas.github.com/coffee-script/ diff --git a/app/helpers/plant_parts_helper.rb b/app/helpers/plant_parts_helper.rb deleted file mode 100644 index b97b124e8..000000000 --- a/app/helpers/plant_parts_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module PlantPartsHelper -end diff --git a/app/helpers/seeds_helper.rb b/app/helpers/seeds_helper.rb deleted file mode 100644 index 46c697f02..000000000 --- a/app/helpers/seeds_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module SeedsHelper -end diff --git a/config/application.rb b/config/application.rb index f1a5e9dd6..6bbe1fe7d 100644 --- a/config/application.rb +++ b/config/application.rb @@ -67,8 +67,12 @@ module Growstuff config.assets.initialize_on_precompile = true config.generators do |g| - g.template_engine :haml - g.stylesheets false + g.template_engine :haml + g.view_specs false + g.controller_specs false + g.helper false + g.stylesheets false + g.javascripts false end config.action_mailer.delivery_method = :sendmail From aebd9e1d4e8a95a65c33ea0f03de5f701fddae85 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 28 Sep 2014 22:03:50 +1000 Subject: [PATCH 040/132] remove a file that accidentally found its way in from another unrelated branch --- spec/features/signup_spec.rb | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 spec/features/signup_spec.rb diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb deleted file mode 100644 index d3be7e17f..000000000 --- a/spec/features/signup_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -feature "signup" do - - scenario "signup" do - visit root_path - first('.signup a').click - - expect(current_path).to eq(new_member_registration_path) - end - - -end From 65c46c334b097816cdc92348a8a390680c4e5a7f Mon Sep 17 00:00:00 2001 From: Skud Date: Mon, 29 Sep 2014 09:18:28 +1000 Subject: [PATCH 041/132] Delete unused photos Make sure the models are setup so that if a photo is not used for anything, it's removed from the system. Also wrote a rake task (which should be run on deploy) to remove older unused photos. --- app/models/harvest.rb | 10 +++++- app/models/photo.rb | 9 ++++- app/models/planting.rb | 10 +++++- db/schema.rb | 4 ++- lib/tasks/growstuff.rake | 9 ++++- spec/models/photo_spec.rb | 76 +++++++++++++++++++++++++++++++++++++++ 6 files changed, 113 insertions(+), 5 deletions(-) diff --git a/app/models/harvest.rb b/app/models/harvest.rb index 8cc54e043..68afb2ee5 100644 --- a/app/models/harvest.rb +++ b/app/models/harvest.rb @@ -10,7 +10,15 @@ class Harvest < ActiveRecord::Base belongs_to :plant_part has_and_belongs_to_many :photos - before_destroy {|harvest| harvest.photos.clear} + + before_destroy do |harvest| + photolist = harvest.photos.to_a # save a temp copy of the photo list + harvest.photos.clear # clear relationship b/w harvest and photo + + photolist.each do |photo| + photo.destroy_if_unused + end + end default_scope order('created_at DESC') diff --git a/app/models/photo.rb b/app/models/photo.rb index 4f0a0160f..32679eeed 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -5,13 +5,20 @@ class Photo < ActiveRecord::Base has_and_belongs_to_many :plantings has_and_belongs_to_many :harvests - before_destroy do |photo| + before_destroy do |photo| photo.plantings.clear photo.harvests.clear end default_scope order("created_at desc") + # remove photos that aren't used by anything + def destroy_if_unused + unless plantings.size > 0 and harvests.size > 0 + self.destroy + end + end + # This is split into a side-effect free method and a side-effecting method # for easier stubbing and testing. def flickr_metadata diff --git a/app/models/planting.rb b/app/models/planting.rb index 270a827d2..8d0a5227a 100644 --- a/app/models/planting.rb +++ b/app/models/planting.rb @@ -11,7 +11,15 @@ class Planting < ActiveRecord::Base belongs_to :crop, :counter_cache => true has_and_belongs_to_many :photos - before_destroy {|planting| planting.photos.clear} + + before_destroy do |planting| + photolist = planting.photos.to_a # save a temp copy of the photo list + planting.photos.clear # clear relationship b/w planting and photo + + photolist.each do |photo| + photo.destroy_if_unused + end + end default_scope order("created_at desc") scope :finished, where(:finished => true) diff --git a/db/schema.rb b/db/schema.rb index 5ad7d3192..3790e9340 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140909001730) do +ActiveRecord::Schema.define(:version => 20140928085713) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -144,6 +144,7 @@ ActiveRecord::Schema.define(:version => 20140909001730) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" + t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true @@ -243,6 +244,7 @@ ActiveRecord::Schema.define(:version => 20140909001730) do t.datetime "updated_at", :null => false t.string "slug" t.integer "forum_id" + t.integer "parent_id" end add_index "posts", ["created_at", "author_id"], :name => "index_updates_on_created_at_and_user_id" diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 34739217b..0ae08a699 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -249,13 +249,20 @@ namespace :growstuff do desc "August 2014: fix ping to pint in database" task :ping_to_pint => :environment do Harvest.find_each do |h| - if h.unit == "ping" + if h.unit == "ping" h.unit = "pint" h.save end end end + desc "October 2014: remove unused photos" + task :remove_unused_photos => :environment do + Photo.find_each do |p| + p.destroy_if_unused + end + end + end # end oneoff section end diff --git a/spec/models/photo_spec.rb b/spec/models/photo_spec.rb index 8ac067ab7..2a0b1d5ff 100644 --- a/spec/models/photo_spec.rb +++ b/spec/models/photo_spec.rb @@ -2,6 +2,82 @@ require 'spec_helper' describe Photo do + describe 'add/delete functionality' do + let(:photo) { FactoryGirl.create(:photo) } + let(:planting) { FactoryGirl.create(:planting) } + let(:harvest) { FactoryGirl.create(:harvest) } + + context "adds photos" do + it 'to a planting' do + planting.photos << photo + expect(planting.photos.count).to eq 1 + expect(planting.photos.first).to eq photo + end + + it 'to a harvest' do + harvest.photos << photo + expect(harvest.photos.count).to eq 1 + expect(harvest.photos.first).to eq photo + end + end + + context "removing photos" do + it 'from a planting' do + planting.photos << photo + photo.destroy + expect(planting.photos.count).to eq 0 + end + + it 'from a harvest' do + harvest.photos << photo + photo.destroy + expect(harvest.photos.count).to eq 0 + end + + it "automatically if unused" do + photo.destroy_if_unused + expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + end + + it 'they are no longer used by plantings' do + planting.photos << photo + planting.destroy # photo is now no longer used by anything + expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + end + + it 'they are no longer used by harvests' do + harvest.photos << photo + harvest.destroy # photo is now no longer used by anything + expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + end + + it 'they are no longer used by anything' do + planting.photos << photo + harvest.photos << photo + expect(photo.plantings.size).to eq 1 + expect(photo.harvests.size).to eq 1 + + planting.destroy # photo is still used by harvest + photo.reload + expect(photo).to be_an_instance_of Photo + expect(photo.plantings.size).to eq 0 + expect(photo.harvests.size).to eq 1 + + harvest.destroy # photo is now no longer used by anything + expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + end + + it 'does not occur when a photo is still in use' do + planting.photos << photo + harvest.photos << photo + planting.destroy # photo is still used by the harvest + expect(photo).to be_an_instance_of Photo + end + + end # removing photos + + end # add/delete functionality + describe 'flickr_metadata' do # Any further tests led to us MOCKING ALL THE THINGS # which was epistemologically unsatisfactory. From e2571e92615bab1da1b38b816e47ab154018dd0c Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 30 Sep 2014 22:37:19 +1000 Subject: [PATCH 042/132] Replaced ENV vars with site_name --- app/views/notifier/planting_reminder.html.haml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/views/notifier/planting_reminder.html.haml b/app/views/notifier/planting_reminder.html.haml index eb7489184..7edf063b5 100644 --- a/app/views/notifier/planting_reminder.html.haml +++ b/app/views/notifier/planting_reminder.html.haml @@ -7,7 +7,7 @@ - if @member.plantings.size == 0 %p - #{ENV['GROWSTUFF_SITE_NAME']} lets you track what food you're growing + #{site_name} lets you track what food you're growing in your garden and see what other people near you are planting too. %p = link_to "Get started now", new_planting_url @@ -33,7 +33,7 @@ - if @member.harvests.size == 0 %p - #{ENV['GROWSTUFF_SITE_NAME']} helps you keep track of what you + #{site_name} helps you keep track of what you harvest from your garden. Record what food you've grown and see what other people near you are harvesting, too. @@ -57,7 +57,7 @@ = link_to "Track your harvests here.", new_harvest_url %h2 - See you soon on #{ENV['GROWSTUFF_SITE_NAME']}! + See you soon on #{site_name}! %p The #{site_name} team. From 98da785d8a52e4272fb405558f759ce34235a894 Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 30 Sep 2014 22:37:58 +1000 Subject: [PATCH 043/132] Removed spurious debugging puts --- lib/tasks/growstuff.rake | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 8ae3b8659..6009e33c6 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -47,7 +47,6 @@ namespace :growstuff do every_n_weeks = 2 # send fortnightly if Date.today.cwday == send_on_day and Date.today.cw_week % every_n_weeks == 0 - puts "We're going to send email because it's the right day" Member.find_each do |m| Notifier.planting_reminder(m).deliver! end From 97699e5e6aff32803c7e5aea6243c2e37a4c3984 Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 30 Sep 2014 22:38:24 +1000 Subject: [PATCH 044/132] Removed feature test that doesn't really test anything featurelike --- spec/features/planting_reminder_spec.rb | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 spec/features/planting_reminder_spec.rb diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb deleted file mode 100644 index 9979ddd99..000000000 --- a/spec/features/planting_reminder_spec.rb +++ /dev/null @@ -1,14 +0,0 @@ -require 'spec_helper' - -feature "planting reminder" do - before :each do - @member = FactoryGirl.create(:member) - end - - scenario "sends email" do - expect { - # stub for while we're working on this. remove! - Notifier.planting_reminder(@member).deliver! - }.to change { ActionMailer::Base.deliveries.count }.by(1) - end -end From 8497782847f491ebee838c75115e23995c098d5b Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 30 Sep 2014 23:02:06 +1000 Subject: [PATCH 045/132] Clear crop hierarchy view cache after uploading crops The crop_sweeper only acts in response to things that happen in the controller. Since this works directly with the model, we need to clear the cache fragment manually. --- lib/tasks/growstuff.rake | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 34739217b..65a418201 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -30,6 +30,7 @@ namespace :growstuff do CSV.foreach(@file) do |row| Crop.create_from_csv(row) end + Rails.cache.delete('full_crop_hierarchy') puts "Finished loading crops" end From 9a9eeecb46b8c2e3fba4ff7d33ce05674eee2ce8 Mon Sep 17 00:00:00 2001 From: Skud Date: Thu, 2 Oct 2014 10:09:21 +1000 Subject: [PATCH 046/132] Removing tests for migrations These were never run anyway (they don't have _spec.rb in their names) and were obviously written when we had no idea what we were doing. They're full of syntax errors and all sorts of stuff that just won't work. The best thing is to remove them so they don't confuse people! --- spec/migrations/give_each_user_a_garden.spec | 12 ------------ spec/migrations/set_up_test_users.spec | 10 ---------- 2 files changed, 22 deletions(-) delete mode 100644 spec/migrations/give_each_user_a_garden.spec delete mode 100644 spec/migrations/set_up_test_users.spec diff --git a/spec/migrations/give_each_user_a_garden.spec b/spec/migrations/give_each_user_a_garden.spec deleted file mode 100644 index 1c6972e54..000000000 --- a/spec/migrations/give_each_user_a_garden.spec +++ /dev/null @@ -1,12 +0,0 @@ -require 'spec_helper' - -describe 'new gardens' do - it "should have 'my garden' for each user" do - (1..3).each do |i| - @user = User.find_by_username("test#{i}") - @garden = Garden.find(:name => "My Garden", :user_id => @user.id) - @garden.should_exist - @garden.slug.should == "test#{i}-my-garden" - end - end -end diff --git a/spec/migrations/set_up_test_users.spec b/spec/migrations/set_up_test_users.spec deleted file mode 100644 index d1f69d3f6..000000000 --- a/spec/migrations/set_up_test_users.spec +++ /dev/null @@ -1,10 +0,0 @@ -require 'spec_helper' - -describe 'test users' do - it 'should have 3 test users' do - (1..3).each do |i| - @user = User.find_by_username("test#{i}") - @user.email.should == "test#{i}@example.com" - end - end -end From 0ad217c9a264cdeb9899362ad9eb098eaf8defa4 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Wed, 1 Oct 2014 20:16:53 -0400 Subject: [PATCH 047/132] add index to harvest_photo --- db/migrate/20140905001730_add_harvests_photos_table.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/db/migrate/20140905001730_add_harvests_photos_table.rb b/db/migrate/20140905001730_add_harvests_photos_table.rb index edacd061b..299d6653e 100644 --- a/db/migrate/20140905001730_add_harvests_photos_table.rb +++ b/db/migrate/20140905001730_add_harvests_photos_table.rb @@ -5,4 +5,6 @@ class AddHarvestsPhotosTable < ActiveRecord::Migration t.integer :harvest_id end end + + add_index(:harvests_photos, [:harvest_id, :photo_id]) end From a29d11a07c24d5da516cc35f7993c584982317ad Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Wed, 1 Oct 2014 20:17:11 -0400 Subject: [PATCH 048/132] refactor error handling on photo upload to be easier to read --- app/controllers/photos_controller.rb | 50 ++++++++++++++-------------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index f4004ad73..9c53e9168 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -67,31 +67,31 @@ class PhotosController < ApplicationController # several models can have photos. we need to know what model and the id # for the entry to attach the photo to valid_models = ["planting", "harvest"] - if params[:type] - if valid_models.include?(params[:type]) - if params[:id] - item = params[:type].camelcase.constantize.find_by_id(params[:id]) - if item - if item.owner.id == current_member.id - # This syntax is weird, so just know that it means this: - # @photo.harvests << item unless @photo.harvests.include?(item) - # but with the correct many-to-many relationship automatically referenced - (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) - else - flash[:alert] = "You must own both the #{params[:type]} and the photo." - end - else - flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." - end - else - flash[:alert] = "Missing id parameter" - end - else - flash[:alert] = "Cannot attach photos to #{params[:type]}" - end - else - flash[:alert] = "Missing type parameter" - end + if ! params[:type] + flash[:alert] = "Missing type parameter" + return 1 + end + if ! valid_models.include?(params[:type]) + flash[:alert] = "Cannot attach photos to #{params[:type]}" + return 1 + end + if ! params[:id] + flash[:alert] = "Missing id parameter" + return 1 + end + item = params[:type].camelcase.constantize.find_by_id(params[:id]) + if ! item + flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." + return 1 + end + if item.owner.id != current_member.id + flash[:alert] = "You must own both the #{params[:type]} and the photo." + return 1 + end + # This syntax is weird, so just know that it means this: + # @photo.harvests << item unless @photo.harvests.include?(item) + # but with the correct many-to-many relationship automatically referenced + (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) respond_to do |format| if @photo.save From 557d07d2f1093009806e3eec323517911288b941 Mon Sep 17 00:00:00 2001 From: Wendy Smoak Date: Wed, 1 Oct 2014 20:24:15 -0400 Subject: [PATCH 049/132] Rename Sign in tests and add Sign up tests. --- spec/features/{signin.rb => signin_spec.rb} | 0 spec/features/signup_spec.rb | 52 +++++++++++++++++++++ 2 files changed, 52 insertions(+) rename spec/features/{signin.rb => signin_spec.rb} (100%) create mode 100644 spec/features/signup_spec.rb diff --git a/spec/features/signin.rb b/spec/features/signin_spec.rb similarity index 100% rename from spec/features/signin.rb rename to spec/features/signin_spec.rb diff --git a/spec/features/signup_spec.rb b/spec/features/signup_spec.rb new file mode 100644 index 000000000..9962df359 --- /dev/null +++ b/spec/features/signup_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature "signup" do + + scenario "sign up for new account from top menubar" do + visit crops_path # something other than front page, which has multiple signup links + click_link 'Sign up' + fill_in 'Login name', with: 'person123' + fill_in 'Email', with: 'gardener@example.com' + fill_in 'Password', with: 'abc123' + fill_in 'Password confirmation', with: 'abc123' + check 'member_tos_agreement' + click_button 'Sign up' + page.has_content? 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' + current_path.should eq root_path + end + + scenario "sign up for new account with existing username" do + visit crops_path # something other than front page, which has multiple signup links + click_link 'Sign up' + fill_in 'Login name', with: 'person123' + fill_in 'Email', with: 'gardener@example.com' + fill_in 'Password', with: 'abc123' + fill_in 'Password confirmation', with: 'abc123' + check 'member_tos_agreement' + click_button 'Sign up' + page.has_content? 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.' + current_path.should eq root_path + first('.signup a').click # click the 'Sign up' button in the middle of the page + fill_in 'Login name', with: 'person123' + fill_in 'Email', with: 'gardener@example.com' + fill_in 'Password', with: 'abc123' + fill_in 'Password confirmation', with: 'abc123' + check 'member_tos_agreement' + click_button 'Sign up' + page.has_content? 'Login name has already been taken' + end + + scenario "sign up for new account without accepting TOS" do + visit root_path + first('.signup a').click # click the 'Sign up' button in the middle of the page + fill_in 'Login name', with: 'person123' + fill_in 'Email', with: 'gardener@example.com' + fill_in 'Password', with: 'abc123' + fill_in 'Password confirmation', with: 'abc123' + # do not check 'member_tos_agreement' + click_button 'Sign up' + page.has_content? 'Tos agreement must be accepted' + current_path.should eq members_path + end + +end From 57abfa04b38f23d15bed82572ce44319279b7edb Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Wed, 1 Oct 2014 21:46:15 -0400 Subject: [PATCH 050/132] fix whitespace --- app/controllers/photos_controller.rb | 58 ++++++++++++++-------------- 1 file changed, 29 insertions(+), 29 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 9c53e9168..a22a3c2f1 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -64,35 +64,35 @@ class PhotosController < ApplicationController @photo.owner_id = current_member.id @photo.set_flickr_metadata - # several models can have photos. we need to know what model and the id - # for the entry to attach the photo to - valid_models = ["planting", "harvest"] - if ! params[:type] - flash[:alert] = "Missing type parameter" - return 1 - end - if ! valid_models.include?(params[:type]) - flash[:alert] = "Cannot attach photos to #{params[:type]}" - return 1 - end - if ! params[:id] - flash[:alert] = "Missing id parameter" - return 1 - end - item = params[:type].camelcase.constantize.find_by_id(params[:id]) - if ! item - flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." - return 1 - end - if item.owner.id != current_member.id - flash[:alert] = "You must own both the #{params[:type]} and the photo." - return 1 - end - # This syntax is weird, so just know that it means this: - # @photo.harvests << item unless @photo.harvests.include?(item) - # but with the correct many-to-many relationship automatically referenced - (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) - + # several models can have photos. we need to know what model and the id + # for the entry to attach the photo to + valid_models = ["planting", "harvest"] + if ! params[:type] + flash[:alert] = "Missing type parameter" + return 1 + end + if ! valid_models.include?(params[:type]) + flash[:alert] = "Cannot attach photos to #{params[:type]}" + return 1 + end + if ! params[:id] + flash[:alert] = "Missing id parameter" + return 1 + end + item = params[:type].camelcase.constantize.find_by_id(params[:id]) + if ! item + flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." + return 1 + end + if item.owner.id != current_member.id + flash[:alert] = "You must own both the #{params[:type]} and the photo." + return 1 + end + # This syntax is weird, so just know that it means this: + # @photo.harvests << item unless @photo.harvests.include?(item) + # but with the correct many-to-many relationship automatically referenced + (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) + respond_to do |format| if @photo.save format.html { redirect_to @photo, notice: 'Photo was successfully added.' } From fcda8742d8049bac9bcfb46f82b00f79c54c4f69 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Wed, 1 Oct 2014 22:52:52 -0400 Subject: [PATCH 051/132] go back to the nested error handling in the photos controller and add a migration for indexing harvests_photos --- app/controllers/photos_controller.rb | 48 +++++++++---------- ...0140905001730_add_harvests_photos_table.rb | 2 - ...41002022459_create_index_harvest_photos.rb | 5 ++ db/schema.rb | 6 +-- 4 files changed, 32 insertions(+), 29 deletions(-) create mode 100644 db/migrate/20141002022459_create_index_harvest_photos.rb diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index a22a3c2f1..1bb9ea4cf 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -67,31 +67,31 @@ class PhotosController < ApplicationController # several models can have photos. we need to know what model and the id # for the entry to attach the photo to valid_models = ["planting", "harvest"] - if ! params[:type] + if params[:type] + if valid_models.include?(params[:type]) + if params[:id] + item = params[:type].camelcase.constantize.find_by_id(params[:id]) + if item + if item.owner.id == current_member.id + # This syntax is weird, so just know that it means this: + # @photo.harvests << item unless @photo.harvests.include?(item) + # but with the correct many-to-many relationship automatically referenced + (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) + else + flash[:alert] = "You must own both the #{params[:type]} and the photo." + end + else + flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." + end + else + flash[:alert] = "Missing id parameter" + end + else + flash[:alert] = "Cannot attach photos to #{params[:type]}" + end + else flash[:alert] = "Missing type parameter" - return 1 - end - if ! valid_models.include?(params[:type]) - flash[:alert] = "Cannot attach photos to #{params[:type]}" - return 1 - end - if ! params[:id] - flash[:alert] = "Missing id parameter" - return 1 - end - item = params[:type].camelcase.constantize.find_by_id(params[:id]) - if ! item - flash[:alert] = "Couldn't find #{params[:type]} to connect to photo." - return 1 - end - if item.owner.id != current_member.id - flash[:alert] = "You must own both the #{params[:type]} and the photo." - return 1 - end - # This syntax is weird, so just know that it means this: - # @photo.harvests << item unless @photo.harvests.include?(item) - # but with the correct many-to-many relationship automatically referenced - (@photo.send "#{params[:type]}s") << item unless (@photo.send "#{params[:type]}s").include?(item) + end respond_to do |format| if @photo.save diff --git a/db/migrate/20140905001730_add_harvests_photos_table.rb b/db/migrate/20140905001730_add_harvests_photos_table.rb index 299d6653e..edacd061b 100644 --- a/db/migrate/20140905001730_add_harvests_photos_table.rb +++ b/db/migrate/20140905001730_add_harvests_photos_table.rb @@ -5,6 +5,4 @@ class AddHarvestsPhotosTable < ActiveRecord::Migration t.integer :harvest_id end end - - add_index(:harvests_photos, [:harvest_id, :photo_id]) end diff --git a/db/migrate/20141002022459_create_index_harvest_photos.rb b/db/migrate/20141002022459_create_index_harvest_photos.rb new file mode 100644 index 000000000..b3bd4317a --- /dev/null +++ b/db/migrate/20141002022459_create_index_harvest_photos.rb @@ -0,0 +1,5 @@ +class CreateIndexHarvestPhotos < ActiveRecord::Migration + def change + add_index(:harvests_photos, [:harvest_id, :photo_id]) + end +end diff --git a/db/schema.rb b/db/schema.rb index 3790e9340..31f3b4487 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140928085713) do +ActiveRecord::Schema.define(:version => 20141002022459) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -113,6 +113,8 @@ ActiveRecord::Schema.define(:version => 20140928085713) do t.integer "harvest_id" end + add_index "harvests_photos", ["harvest_id", "photo_id"], :name => "index_harvests_photos_on_harvest_id_and_photo_id" + create_table "members", :force => true do |t| t.string "email", :default => "", :null => false t.string "encrypted_password", :default => "", :null => false @@ -144,7 +146,6 @@ ActiveRecord::Schema.define(:version => 20140928085713) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" - t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true @@ -244,7 +245,6 @@ ActiveRecord::Schema.define(:version => 20140928085713) do t.datetime "updated_at", :null => false t.string "slug" t.integer "forum_id" - t.integer "parent_id" end add_index "posts", ["created_at", "author_id"], :name => "index_updates_on_created_at_and_user_id" From 48ad561b76c4128928650c8bde26dc6109383278 Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Wed, 1 Oct 2014 23:58:54 -0400 Subject: [PATCH 052/132] convert tabs to spaces --- app/controllers/photos_controller.rb | 2 +- ...41002022459_create_index_harvest_photos.rb | 2 +- spec/models/harvest_spec.rb | 52 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/app/controllers/photos_controller.rb b/app/controllers/photos_controller.rb index 1bb9ea4cf..45755db84 100644 --- a/app/controllers/photos_controller.rb +++ b/app/controllers/photos_controller.rb @@ -91,7 +91,7 @@ class PhotosController < ApplicationController end else flash[:alert] = "Missing type parameter" - end + end respond_to do |format| if @photo.save diff --git a/db/migrate/20141002022459_create_index_harvest_photos.rb b/db/migrate/20141002022459_create_index_harvest_photos.rb index b3bd4317a..75ef69d36 100644 --- a/db/migrate/20141002022459_create_index_harvest_photos.rb +++ b/db/migrate/20141002022459_create_index_harvest_photos.rb @@ -1,5 +1,5 @@ class CreateIndexHarvestPhotos < ActiveRecord::Migration def change - add_index(:harvests_photos, [:harvest_id, :photo_id]) + add_index(:harvests_photos, [:harvest_id, :photo_id]) end end diff --git a/spec/models/harvest_spec.rb b/spec/models/harvest_spec.rb index a5b5cabf7..66c83c93e 100644 --- a/spec/models/harvest_spec.rb +++ b/spec/models/harvest_spec.rb @@ -127,30 +127,30 @@ describe Harvest do end context 'photos' do - before(:each) do - @harvest = FactoryGirl.create(:harvest) - @photo = FactoryGirl.create(:photo) - @harvest.photos << @photo - end - - it 'has a photo' do - @harvest.photos.first.should eq @photo - end - - it 'deletes association with photos when photo is deleted' do - @photo.destroy - @harvest.reload - @harvest.photos.should be_empty - end - - it 'has a default photo' do - @harvest.default_photo.should eq @photo - end - - it 'chooses the most recent photo' do - @photo2 = FactoryGirl.create(:photo) - @harvest.photos << @photo2 - @harvest.default_photo.should eq @photo2 - end - end + before(:each) do + @harvest = FactoryGirl.create(:harvest) + @photo = FactoryGirl.create(:photo) + @harvest.photos << @photo + end + + it 'has a photo' do + @harvest.photos.first.should eq @photo + end + + it 'deletes association with photos when photo is deleted' do + @photo.destroy + @harvest.reload + @harvest.photos.should be_empty + end + + it 'has a default photo' do + @harvest.default_photo.should eq @photo + end + + it 'chooses the most recent photo' do + @photo2 = FactoryGirl.create(:photo) + @harvest.photos << @photo2 + @harvest.default_photo.should eq @photo2 + end + end end From e002cb2d35dcd209fb462f4d8bed4b81cdcef9ec Mon Sep 17 00:00:00 2001 From: Skud Date: Thu, 2 Oct 2014 17:48:19 +1000 Subject: [PATCH 053/132] added deploy task to remove unused photos --- script/deploy-tasks.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/script/deploy-tasks.sh b/script/deploy-tasks.sh index ac4068b26..3c6dfb909 100755 --- a/script/deploy-tasks.sh +++ b/script/deploy-tasks.sh @@ -12,3 +12,6 @@ echo "2014-09-28 - upload tomatoes" rake growstuff:import_crops file=db/seeds/crops-11-tomatoes.csv + +echo "2014-10-02 - remove unused photos" +rake growstuff:oneoff:remove_unused_photos From a2f2508f0ddd9080fcb793da0ca1415c4df6de7d Mon Sep 17 00:00:00 2001 From: Mackenzie Morgan Date: Thu, 2 Oct 2014 09:38:02 -0400 Subject: [PATCH 054/132] don't lose the thing we're attaching the photo to when we change sets. This is a modification of oshiho3's previous fix. --- app/views/photos/new.html.haml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index f0503a6a6..bc3eb35df 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -15,7 +15,8 @@ = form_tag(new_photo_path, :method => :get, :class => 'form-inline') do = label_tag :set, "Choose a photo set:", :class => 'control-label' = select_tag :set, options_for_select(@sets, @current_set), :class => 'input-large' - = hidden_field_tag :planting_id, @planting_id + = hidden_field_tag :type, @type + = hidden_field_tag :id, @id = submit_tag "Search", :class => 'btn btn-primary' %div.pagination From 14d039ee018b4d877df10c17285d7a0f471fecd4 Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Mon, 29 Sep 2014 22:37:03 +1000 Subject: [PATCH 055/132] Added crops-posts association as well as updated GUI for crop show --- app/controllers/crops_controller.rb | 1 + app/controllers/harvests_controller.rb | 18 +++--- app/controllers/plantings_controller.rb | 18 +++--- app/models/crop.rb | 3 + app/models/post.rb | 14 ++++ app/views/crops/_harvests.html.haml | 8 ++- app/views/crops/_plantings.html.haml | 21 ++++++ app/views/crops/show.html.haml | 14 ++-- app/views/harvests/index.html.haml | 2 +- app/views/plantings/index.html.haml | 2 +- config/routes.rb | 2 + .../20140928044231_add_crops_posts_table.rb | 10 +++ db/schema.rb | 10 ++- lib/haml/filters/growstuff_markdown.rb | 3 +- lib/tasks/growstuff.rake | 7 ++ spec/controllers/harvests_controller_spec.rb | 24 ++++++- spec/controllers/plantings_controller_spec.rb | 28 ++++++-- spec/models/crop_spec.rb | 18 ++++++ spec/models/post_spec.rb | 37 +++++++++++ spec/views/crops/show.html.haml_spec.rb | 64 ++++++++++++++++--- spec/views/harvests/index.html.haml_spec.rb | 13 ++++ spec/views/plantings/index.html.haml_spec.rb | 11 ++++ 22 files changed, 281 insertions(+), 47 deletions(-) create mode 100644 app/views/crops/_plantings.html.haml create mode 100644 db/migrate/20140928044231_add_crops_posts_table.rb diff --git a/app/controllers/crops_controller.rb b/app/controllers/crops_controller.rb index 1316350de..a30de159a 100644 --- a/app/controllers/crops_controller.rb +++ b/app/controllers/crops_controller.rb @@ -69,6 +69,7 @@ class CropsController < ApplicationController # GET /crops/1.json def show @crop = Crop.includes(:scientific_names, {:plantings => :photos}).find(params[:id]) + @posts = @crop.posts.paginate(:page => params[:page]) respond_to do |format| format.html # show.html.haml diff --git a/app/controllers/harvests_controller.rb b/app/controllers/harvests_controller.rb index 96e029ac3..524007d47 100644 --- a/app/controllers/harvests_controller.rb +++ b/app/controllers/harvests_controller.rb @@ -6,23 +6,21 @@ class HarvestsController < ApplicationController # GET /harvests.json def index @owner = Member.find_by_slug(params[:owner]) + @crop = Crop.find_by_slug(params[:crop]) if @owner - @harvests = @owner.harvests.includes(:owner, :crop).paginate(:page => params[:page]) + @harvests = @owner.harvests.includes(:owner, :crop) + elsif @crop + @harvests = @crop.harvests.includes(:owner, :crop) else - @harvests = Harvest.includes(:owner, :crop).paginate(:page => params[:page]) + @harvests = Harvest.includes(:owner, :crop) end respond_to do |format| - format.html # index.html.erb + format.html { @harvests = @harvests.paginate(:page => params[:page]) } format.json { render json: @harvests } format.csv do - if @owner - @filename = "Growstuff-#{@owner}-Harvests-#{Time.zone.now.to_s(:number)}.csv" - @harvests = @owner.harvests.includes(:owner, :crop) - else - @filename = "Growstuff-Harvests-#{Time.zone.now.to_s(:number)}.csv" - @harvests = Harvest.includes(:owner, :crop) - end + specifics = (@owner ? "#{@owner.name}-" : @crop ? "#{@crop.name}-" : nil) + @filename = "Growstuff-#{specifics}Harvests-#{Time.zone.now.to_s(:number)}.csv" render :csv => @harvests end end diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index 0d419d2f1..62eaf5b79 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -7,24 +7,22 @@ class PlantingsController < ApplicationController # GET /plantings.json def index @owner = Member.find_by_slug(params[:owner]) + @crop = Crop.find_by_slug(params[:crop]) if @owner - @plantings = @owner.plantings.includes(:owner, :crop, :garden).paginate(:page => params[:page]) + @plantings = @owner.plantings.includes(:owner, :crop, :garden) + elsif @crop + @plantings = @crop.plantings.includes(:owner, :crop, :garden) else - @plantings = Planting.includes(:owner, :crop, :garden).paginate(:page => params[:page]) + @plantings = Planting.includes(:owner, :crop, :garden) end respond_to do |format| - format.html # index.html.erb + format.html { @plantings = @plantings.paginate(:page => params[:page]) } format.json { render json: @plantings } format.rss { render :layout => false } #index.rss.builder format.csv do - if @owner - @filename = "Growstuff-#{@owner}-Plantings-#{Time.zone.now.to_s(:number)}.csv" - @plantings = @owner.plantings.includes(:owner, :crop, :garden) - else - @filename = "Growstuff-Plantings-#{Time.zone.now.to_s(:number)}.csv" - @plantings = Planting.includes(:owner, :crop, :garden) - end + specifics = (@owner ? "#{@owner.name}-" : @crop ? "#{@crop.name}-" : nil) + @filename = "Growstuff-#{specifics}Plantings-#{Time.zone.now.to_s(:number)}.csv" render :csv => @plantings end end diff --git a/app/models/crop.rb b/app/models/crop.rb index 3efe8be57..387936d91 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -17,6 +17,9 @@ class Crop < ActiveRecord::Base belongs_to :parent, :class_name => 'Crop' has_many :varieties, :class_name => 'Crop', :foreign_key => 'parent_id' + has_and_belongs_to_many :posts + before_destroy {|crop| crop.posts.clear} + default_scope order("lower(name) asc") scope :recent, reorder("created_at desc") diff --git a/app/models/post.rb b/app/models/post.rb index 3a853c18a..7f72a4e57 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -5,6 +5,9 @@ class Post < ActiveRecord::Base belongs_to :author, :class_name => 'Member' belongs_to :forum has_many :comments, :dependent => :destroy + has_and_belongs_to_many :crops + before_destroy {|post| post.crops.clear} + after_save :update_crops_posts_association # also has_many notifications, but kinda meaningless to get at them # from this direction, so we won't set up an association for now. @@ -39,4 +42,15 @@ class Post < ActiveRecord::Base end end + private + def update_crops_posts_association + self.crops.destroy_all + # look for crops mentioned in the post. eg. [tomato](crop) + self.body.scan(Haml::Filters::GrowstuffMarkdown::CROP_REGEX) do |m| + # find crop case-insensitively + crop = Crop.where('lower(name) = ?', $1.downcase).first + # create association + self.crops << crop if crop and not self.crops.include?(crop) + end + end end diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index b8cb25447..d10f57d06 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -4,16 +4,18 @@ Nobody has harvested this crop yet. - else %ul - - crop.harvests.each do |harvest| + - crop.harvests.take(3).each do |harvest| %li = link_to "#{harvest.owner} harvested #{display_quantity(harvest)}.", harvest_path(harvest) = render :partial => 'members/location', :locals => { :member => harvest.owner } %small = distance_of_time_in_words(harvest.created_at, Time.zone.now) ago. - + %p.col-md-offset-1 + = link_to "See all #{crop.name} harvests", harvests_by_crop_path(crop) - if current_member - = link_to "Track your #{crop.name} harvests.", new_harvest_path() + %p.col-md-offset-1 + = link_to "Track your #{crop.name} harvests.", new_harvest_path(:crop_id => crop.id) - else = render :partial => 'shared/signin_signup', :locals => { :to => "track your #{crop.name} harvests" } diff --git a/app/views/crops/_plantings.html.haml b/app/views/crops/_plantings.html.haml new file mode 100644 index 000000000..72bce5608 --- /dev/null +++ b/app/views/crops/_plantings.html.haml @@ -0,0 +1,21 @@ +%h4 Plantings +- if crop.plantings.empty? + %p + Nobody has planted this crop yet. +- else + %ul + - crop.plantings.take(3).each do |planting| + %li + = link_to "#{planting.owner} planted #{planting.quantity} #{planting.planted_from}.", planting_path(planting) + = render :partial => 'members/location', :locals => { :member => planting.owner } + %small + = distance_of_time_in_words(planting.created_at, Time.zone.now) + ago. + %p.col-md-offset-1 + = link_to "See all #{crop.name} plantings", plantings_by_crop_path(crop) +- if current_member + %p.col-md-offset-1 + = link_to "Track your #{crop.name} plantings.", new_planting_path(:crop_id => crop.id) +- else + = render :partial => 'shared/signin_signup', :locals => { :to => "track your #{crop.name} plantings" } + diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index b79320698..d28084280 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -33,12 +33,17 @@ %div#cropmap - - if @crop.plantings.size > 0 + %div.pagination + = page_entries_info @posts, :model => "posts" + = will_paginate @posts - %h2 All plantings + - unless @posts.empty? + - @posts.each do |post| + = render :partial => "posts/single", :locals => { :post => post, :subject => true } - - @crop.plantings.each do |p| - = render :partial => "plantings/thumbnail", :locals => { :planting => p, :title => 'owner' } + %div.pagination + = page_entries_info @posts, :model => "posts" + = will_paginate @posts .col-md-3 - if can? :edit, @crop or can? :destroy, @crop @@ -76,5 +81,6 @@ %ul %li= link_to 'Wikipedia (English)', @crop.en_wikipedia_url + = render :partial => 'plantings', :locals => { :crop => @crop } = render :partial => 'harvests', :locals => { :crop => @crop } = render :partial => 'find_seeds', :locals => { :crop => @crop } diff --git a/app/views/harvests/index.html.haml b/app/views/harvests/index.html.haml index 90bbb4638..f80ce0dd5 100644 --- a/app/views/harvests/index.html.haml +++ b/app/views/harvests/index.html.haml @@ -1,4 +1,4 @@ -- content_for :title, @owner ? "#{@owner}'s harvests" : "Everyone's harvests" +- content_for :title, @owner ? "#{@owner}'s harvests" : @crop ? "Everyone's #{@crop.name} harvests" : "Everyone's harvests" %p #{ENV['GROWSTUFF_SITE_NAME']} helps you track what you're diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index 091974ef8..eb1e017dc 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -1,4 +1,4 @@ -- content_for :title, @owner ? "#{@owner}'s plantings" : "Everyone's plantings" +- content_for :title, @owner ? "#{@owner}'s plantings" : @crop ? "Everyone's #{@crop.name} plantings" : "Everyone's plantings" %p - if can? :create, Planting diff --git a/config/routes.rb b/config/routes.rb index 360ed2477..0d645a1be 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -12,6 +12,7 @@ Growstuff::Application.routes.draw do resources :plantings match '/plantings/owner/:owner' => 'plantings#index', :as => 'plantings_by_owner' + match '/plantings/crop/:crop' => 'plantings#index', :as => 'plantings_by_crop' resources :gardens match '/gardens/owner/:owner' => 'gardens#index', :as => 'gardens_by_owner' @@ -21,6 +22,7 @@ Growstuff::Application.routes.draw do resources :harvests match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' + match '/harvests/crop/:crop' => 'harvests#index', :as => 'harvests_by_crop' resources :posts match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' diff --git a/db/migrate/20140928044231_add_crops_posts_table.rb b/db/migrate/20140928044231_add_crops_posts_table.rb new file mode 100644 index 000000000..a8c06927b --- /dev/null +++ b/db/migrate/20140928044231_add_crops_posts_table.rb @@ -0,0 +1,10 @@ +class AddCropsPostsTable < ActiveRecord::Migration + def change + create_table :crops_posts, :id => false do |t| + t.integer :crop_id + t.integer :post_id + end + add_index :crops_posts, [:crop_id, :post_id] + add_index :crops_posts, :crop_id + end +end \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 29271f5e6..e103777e6 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20140829230600) do +ActiveRecord::Schema.define(:version => 20140928053257) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -64,6 +64,14 @@ ActiveRecord::Schema.define(:version => 20140829230600) do add_index "crops", ["name"], :name => "index_crops_on_name" add_index "crops", ["slug"], :name => "index_crops_on_slug", :unique => true + create_table "crops_posts", :id => false, :force => true do |t| + t.integer "crop_id" + t.integer "post_id" + end + + add_index "crops_posts", ["crop_id", "post_id"], :name => "index_crops_posts_on_crop_id_and_post_id" + add_index "crops_posts", ["crop_id"], :name => "index_crops_posts_on_crop_id" + create_table "forums", :force => true do |t| t.string "name", :null => false t.text "description", :null => false diff --git a/lib/haml/filters/growstuff_markdown.rb b/lib/haml/filters/growstuff_markdown.rb index 7069590e7..dba081e44 100644 --- a/lib/haml/filters/growstuff_markdown.rb +++ b/lib/haml/filters/growstuff_markdown.rb @@ -2,12 +2,13 @@ require 'bluecloth' module Haml::Filters module GrowstuffMarkdown + CROP_REGEX = /\[([^\[\]]+?)\]\(crop\)/ include Haml::Filters::Base def render(text) # turn [tomato](crop) into [tomato](http://growstuff.org/crops/tomato) - expanded = text.gsub(/\[([^\[\]]+?)\]\(crop\)/) do |m| + expanded = text.gsub(CROP_REGEX) do |m| crop_str = $1 # find crop case-insensitively crop = Crop.where('lower(name) = ?', crop_str.downcase).first diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 34739217b..3c4a676bc 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -256,6 +256,13 @@ namespace :growstuff do end end + desc "October 2014: generate crops_posts records for existing posts" + task :generate_crops_posts_records => :environment do + Post.find_each do |p| + p.send :update_crops_posts_association + end + end + end # end oneoff section end diff --git a/spec/controllers/harvests_controller_spec.rb b/spec/controllers/harvests_controller_spec.rb index f2ecb8e32..38b0edf8c 100644 --- a/spec/controllers/harvests_controller_spec.rb +++ b/spec/controllers/harvests_controller_spec.rb @@ -12,10 +12,30 @@ describe HarvestsController do end describe "GET index" do + before do + @member1 = FactoryGirl.create(:member) + @member2 = FactoryGirl.create(:member) + @tomato = FactoryGirl.create(:tomato) + @maize = FactoryGirl.create(:maize) + @harvest1 = FactoryGirl.create(:harvest, :owner_id => @member1.id, :crop_id => @tomato.id) + @harvest2 = FactoryGirl.create(:harvest, :owner_id => @member2.id, :crop_id => @maize.id) + end + it "assigns all harvests as @harvests" do - harvest = Harvest.create! valid_attributes get :index, {} - assigns(:harvests).should eq([harvest]) + assigns(:harvests).should =~ [@harvest1, @harvest2] + end + + it "picks up owner from params and shows owner's harvests only" do + get :index, {:owner => @member1.slug} + assigns(:owner).should eq @member1 + assigns(:harvests).should eq [@harvest1] + end + + it "picks up crop from params and shows the harvests for the crop only" do + get :index, {:crop => @maize.name} + assigns(:crop).should eq @maize + assigns(:harvests).should eq [@harvest2] end end diff --git a/spec/controllers/plantings_controller_spec.rb b/spec/controllers/plantings_controller_spec.rb index 7e6a75d22..4b3a518a5 100644 --- a/spec/controllers/plantings_controller_spec.rb +++ b/spec/controllers/plantings_controller_spec.rb @@ -12,10 +12,30 @@ describe PlantingsController do end describe "GET index" do - it "picks up owner from params" do - owner = FactoryGirl.create(:member) - get :index, {:owner => owner.slug} - assigns(:owner).should eq(owner) + before do + @member1 = FactoryGirl.create(:member) + @member2 = FactoryGirl.create(:member) + @tomato = FactoryGirl.create(:tomato) + @maize = FactoryGirl.create(:maize) + @planting1 = FactoryGirl.create(:planting, :crop => @tomato, :owner => @member1) + @planting2 = FactoryGirl.create(:planting, :crop => @maize, :owner => @member2) + end + + it "assigns all plantings as @plantings" do + get :index, {} + assigns(:plantings).should =~ [@planting1, @planting2] + end + + it "picks up owner from params and shows owner's plantings only" do + get :index, {:owner => @member1.slug} + assigns(:owner).should eq @member1 + assigns(:plantings).should eq [@planting1] + end + + it "picks up crop from params and shows the plantings for the crop only" do + get :index, {:crop => @maize.name} + assigns(:crop).should eq @maize + assigns(:plantings).should eq [@planting2] end end diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index 1741faf74..b54097070 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -343,4 +343,22 @@ describe Crop do end end + context "crop-post association" do + before { + @tomato = FactoryGirl.create(:tomato) + @maize = FactoryGirl.create(:maize) + @post = FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") + } + + describe "destroying a crop" do + before do + @tomato.destroy + end + + it "shouod delete the association but not the posts" do + Post.find_by_id(@post.id).should_not eq nil + Post.find_by_id(@post.id).crops.should eq [@maize] + end + end + end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 441b9c6a9..3751c13c7 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -95,4 +95,41 @@ describe Post do end end + context "crop-post association" do + before { + @tomato = FactoryGirl.create(:tomato) + @maize = FactoryGirl.create(:maize) + @chard = FactoryGirl.create(:chard) + @post = FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") + } + + it "should be generated without duplicate" do + @post.crops.should =~ [@tomato, @maize] + @tomato.posts.should eq [@post] + @maize.posts.should eq [@post] + end + + it "should be updated when post was modified" do + @post.update_attributes(:body => "[chard](crop)") + + @post.crops.should eq [@chard] + @chard.posts.should eq [@post] + @tomato.posts.should eq [] + @maize.posts.should eq [] + end + + describe "destroying the post" do + before do + @crops = @post.crops + @post.destroy + end + + it "shouod delete the association but not the crops" do + Crop.find_by_id(@tomato.id).should_not eq nil + Crop.find_by_id(@maize.id).should_not eq nil + Crop.find_by_id(@tomato.id).posts.should eq [] + Crop.find_by_id(@maize.id).posts.should eq [] + end + end + end end diff --git a/spec/views/crops/show.html.haml_spec.rb b/spec/views/crops/show.html.haml_spec.rb index 6e8250b27..1ef461508 100644 --- a/spec/views/crops/show.html.haml_spec.rb +++ b/spec/views/crops/show.html.haml_spec.rb @@ -7,6 +7,16 @@ describe "crops/show" do :scientific_names => [ FactoryGirl.create(:zea_mays) ] ) assign(:crop, @crop) + @author = FactoryGirl.create(:member) + page = 1 + per_page = 2 + total_entries = 2 + @posts = WillPaginate::Collection.create(page, per_page, total_entries) do |pager| + pager.replace([ + @post1 = FactoryGirl.create(:post, :author => @author, :body => "Post it!" ), + @post2 = FactoryGirl.create(:post, :author => @author, :body => "Done!" ) + ]) + end end context 'photos' do @@ -125,11 +135,10 @@ describe "crops/show" do context "has plantings" do before(:each) do - @owner = FactoryGirl.create(:member) - @garden = FactoryGirl.create(:garden, :owner => @owner) + @owner = FactoryGirl.create(:london_member) @planting = FactoryGirl.create(:planting, - :garden => @garden, - :crop => @crop + :crop => @crop, + :owner => @owner ) @crop.reload # to pick up latest plantings_count end @@ -137,16 +146,24 @@ describe "crops/show" do it "links to people who are growing this crop" do render rendered.should contain @owner.login_name - rendered.should contain @garden.name + rendered.should contain @owner.location end + end - it "shows photos where available" do - @photo = FactoryGirl.create(:photo) - @planting.photos << @photo + context "has posts" do + it "links to posts" do render - assert_select "img", :src => @photo.thumbnail_url + @posts.each do |p| + rendered.should contain p.author.login_name + rendered.should contain p.subject + rendered.should contain p.body + end end + it "contains two gravatar icons" do + render + assert_select "img", :src => /gravatar\.com\/avatar/, :count => 2 + end end context 'varieties' do @@ -189,9 +206,36 @@ describe "crops/show" do rendered.should contain "Harvest this" end - it "links to the right crop in the planting link" do + it "links to the right crop in the new planting link" do assert_select("a[href=#{new_planting_path}?crop_id=#{@crop.id}]") end + + it "links to the right crop in the new harvest link" do + assert_select("a[href=#{new_harvest_path}?crop_id=#{@crop.id}]") + end + + it { rendered.should contain "Nobody has planted this crop yet" } + it { rendered.should contain "Nobody has harvested this crop yet" } + + context "should have a link to" do + before do + FactoryGirl.create(:planting, :crop => @crop) + FactoryGirl.create(:harvest, :crop => @crop) + @crop.reload + render + end + + it "show all plantings by the crop link" do + assert_select("a[href=#{plantings_by_crop_path @crop}]") + end + + it "show all harvests by the crop link" do + assert_select("a[href=#{harvests_by_crop_path @crop}]") + end + end + + + end context "logged in and crop wrangler" do diff --git a/spec/views/harvests/index.html.haml_spec.rb b/spec/views/harvests/index.html.haml_spec.rb index 3a147e0b6..093a383a9 100644 --- a/spec/views/harvests/index.html.haml_spec.rb +++ b/spec/views/harvests/index.html.haml_spec.rb @@ -41,4 +41,17 @@ describe "harvests/index" do assert_select "a", :href => harvests_path(:format => 'csv') assert_select "a", :href => harvests_path(:format => 'json') end + + it "displays member's name in title" do + assign(:owner, @member) + render + view.content_for(:title).should contain @member.login_name + end + + it "displays crop's name in title" do + assign(:crop, @tomato) + render + view.content_for(:title).should contain @tomato.name + end + end diff --git a/spec/views/plantings/index.html.haml_spec.rb b/spec/views/plantings/index.html.haml_spec.rb index 5ff890514..d46b4e003 100644 --- a/spec/views/plantings/index.html.haml_spec.rb +++ b/spec/views/plantings/index.html.haml_spec.rb @@ -48,4 +48,15 @@ describe "plantings/index" do assert_select "a", :href => plantings_path(:format => 'rss') end + it "displays member's name in title" do + assign(:owner, @member) + render + view.content_for(:title).should contain @member.login_name + end + + it "displays crop's name in title" do + assign(:crop, @tomato) + render + view.content_for(:title).should contain @tomato.name + end end From cf07ecfa4aa9e96c8100437653b7088b6bfb5a20 Mon Sep 17 00:00:00 2001 From: Shiho Takagi Date: Sun, 5 Oct 2014 23:16:46 +1100 Subject: [PATCH 056/132] improved test spec, crop page and added rake task --- app/views/crops/show.html.haml | 5 ++-- db/schema.rb | 1 - lib/tasks/growstuff.rake | 2 +- script/deploy-tasks.sh | 3 +++ spec/models/crop_spec.rb | 19 +++++++------- spec/models/post_spec.rb | 47 ++++++++++++++++++---------------- 6 files changed, 42 insertions(+), 35 deletions(-) diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index d28084280..670a0d438 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -33,9 +33,10 @@ %div#cropmap + %a{:name => 'posts'} %div.pagination = page_entries_info @posts, :model => "posts" - = will_paginate @posts + = will_paginate @posts, :params => {:anchor => "posts"} - unless @posts.empty? - @posts.each do |post| @@ -43,7 +44,7 @@ %div.pagination = page_entries_info @posts, :model => "posts" - = will_paginate @posts + = will_paginate @posts, :params => {:anchor => "posts"} .col-md-3 - if can? :edit, @crop or can? :destroy, @crop diff --git a/db/schema.rb b/db/schema.rb index e4425711c..9292a6fbb 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,6 @@ # # It's strongly recommended to check this file into your version control system. - ActiveRecord::Schema.define(:version => 20141002022459) do create_table "account_types", :force => true do |t| diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 62046d87a..f19bf1fed 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -267,7 +267,7 @@ namespace :growstuff do desc "October 2014: generate crops_posts records for existing posts" task :generate_crops_posts_records => :environment do Post.find_each do |p| - p.send :update_crops_posts_association + p.save end end end # end oneoff section diff --git a/script/deploy-tasks.sh b/script/deploy-tasks.sh index 3c6dfb909..451fe0032 100755 --- a/script/deploy-tasks.sh +++ b/script/deploy-tasks.sh @@ -15,3 +15,6 @@ rake growstuff:import_crops file=db/seeds/crops-11-tomatoes.csv echo "2014-10-02 - remove unused photos" rake growstuff:oneoff:remove_unused_photos + +echo "2014-10-05 - generate crops_posts records for existing posts" +rake growstuff:oneoff:generate_crops_posts_records diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index b54097070..d9e3d4b0a 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -344,20 +344,21 @@ describe Crop do end context "crop-post association" do - before { - @tomato = FactoryGirl.create(:tomato) - @maize = FactoryGirl.create(:maize) - @post = FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") - } + let!(:tomato) { FactoryGirl.create(:tomato) } + let!(:maize) { FactoryGirl.create(:maize) } + let!(:post) { FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") } describe "destroying a crop" do before do - @tomato.destroy + tomato.destroy end - it "shouod delete the association but not the posts" do - Post.find_by_id(@post.id).should_not eq nil - Post.find_by_id(@post.id).crops.should eq [@maize] + it "should delete the association between post and the crop(tomato)" do + expect(Post.find(post).crops).to eq [maize] + end + + it "should not delete the posts" do + expect(Post.find(post)).to_not eq nil end end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 3751c13c7..30c19c500 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -96,39 +96,42 @@ describe Post do end context "crop-post association" do - before { - @tomato = FactoryGirl.create(:tomato) - @maize = FactoryGirl.create(:maize) - @chard = FactoryGirl.create(:chard) - @post = FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") - } + let!(:tomato) { FactoryGirl.create(:tomato) } + let!(:maize) { FactoryGirl.create(:maize) } + let!(:chard) { FactoryGirl.create(:chard) } + let!(:post) { FactoryGirl.create(:post, :body => "[maize](crop)[tomato](crop)[tomato](crop)") } - it "should be generated without duplicate" do - @post.crops.should =~ [@tomato, @maize] - @tomato.posts.should eq [@post] - @maize.posts.should eq [@post] + it "should be generated" do + expect(tomato.posts).to eq [post] + expect(maize.posts).to eq [post] + end + + it "should not duplicate" do + expect(post.crops) =~ [tomato, maize] end it "should be updated when post was modified" do - @post.update_attributes(:body => "[chard](crop)") + post.update_attributes(:body => "[chard](crop)") - @post.crops.should eq [@chard] - @chard.posts.should eq [@post] - @tomato.posts.should eq [] - @maize.posts.should eq [] + expect(post.crops).to eq [chard] + expect(chard.posts).to eq [post] + expect(tomato.posts).to eq [] + expect(maize.posts).to eq [] end describe "destroying the post" do before do - @crops = @post.crops - @post.destroy + post.destroy end - it "shouod delete the association but not the crops" do - Crop.find_by_id(@tomato.id).should_not eq nil - Crop.find_by_id(@maize.id).should_not eq nil - Crop.find_by_id(@tomato.id).posts.should eq [] - Crop.find_by_id(@maize.id).posts.should eq [] + it "should delete the association" do + expect(Crop.find(tomato).posts).to eq [] + expect(Crop.find(maize).posts).to eq [] + end + + it "should not delete the crops" do + expect(Crop.find(tomato)).to_not eq nil + expect(Crop.find(maize)).to_not eq nil end end end From ff617397165ab35ff5eec57f81167200b8f658c0 Mon Sep 17 00:00:00 2001 From: Skud Date: Wed, 8 Oct 2014 18:59:10 +0100 Subject: [PATCH 057/132] s/boring_planting/no_photo_planting for clarity --- spec/models/planting_spec.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/spec/models/planting_spec.rb b/spec/models/planting_spec.rb index da9404c4c..0f64a53f1 100644 --- a/spec/models/planting_spec.rb +++ b/spec/models/planting_spec.rb @@ -185,10 +185,10 @@ describe Planting do @planting.save # this one doesn't have a photo - @boring_planting = FactoryGirl.create(:planting) + @no_photo_planting = FactoryGirl.create(:planting) Planting.interesting.should include @planting - Planting.interesting.should_not include @boring_planting + Planting.interesting.should_not include @no_photo_planting end it 'ignores plantings with the same owner' do @@ -219,11 +219,11 @@ describe Planting do @planting.save # this one doesn't have a photo - @boring_planting = FactoryGirl.create(:planting) + @no_photo_planting = FactoryGirl.create(:planting) interesting = Planting.interesting(10, false) interesting.should include @planting - interesting.should include @boring_planting + interesting.should include @no_photo_planting end end From d1e0bdb53445c0a1a7a21f708ba0a5873346605d Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 12 Oct 2014 09:53:09 +1100 Subject: [PATCH 058/132] toggling planting finished clears the finished at field or populates it with a cached value --- Gemfile | 1 + Gemfile.lock | 7 ++++ .../javascripts/finish_planting.js.coffee | 12 +++++++ db/schema.rb | 9 +++++ .../plantings/planting_a_crop_spec.rb | 36 ++++++++++++++----- 5 files changed, 57 insertions(+), 8 deletions(-) create mode 100644 app/assets/javascripts/finish_planting.js.coffee diff --git a/Gemfile b/Gemfile index e47403546..93fd9858d 100644 --- a/Gemfile +++ b/Gemfile @@ -119,6 +119,7 @@ gem 'omniauth-flickr', '>= 0.0.15' gem 'rake', '>= 10.0.0' group :development, :test do + gem 'pry' gem 'haml-rails' # HTML templating language gem 'rspec-rails', '~> 2.12.1' # unit testing framework gem 'database_cleaner', '~> 1.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 7829a34de..6f4b323df 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -173,6 +173,7 @@ GEM mime-types (~> 1.16) treetop (~> 1.4.8) memcachier (0.0.2) + method_source (0.8.2) mime-types (1.25.1) mini_portile (0.6.0) multi_json (1.10.1) @@ -201,6 +202,10 @@ GEM multi_json (~> 1.0) websocket-driver (>= 0.2.0) polyglot (0.3.5) + pry (0.10.0) + coderay (~> 1.1.0) + method_source (~> 0.8.1) + slop (~> 3.4) rack (1.4.5) rack-cache (1.2) rack (>= 0.4) @@ -257,6 +262,7 @@ GEM multi_json simplecov-html (~> 0.8.0) simplecov-html (0.8.0) + slop (3.6.0) sprockets (2.2.2) hike (~> 1.2) multi_json (~> 1.0) @@ -343,6 +349,7 @@ DEPENDENCIES omniauth-twitter pg poltergeist (~> 1.5.1) + pry rack (~> 1.4.5) rails (= 3.2.13) rails_12factor diff --git a/app/assets/javascripts/finish_planting.js.coffee b/app/assets/javascripts/finish_planting.js.coffee new file mode 100644 index 000000000..26f30114b --- /dev/null +++ b/app/assets/javascripts/finish_planting.js.coffee @@ -0,0 +1,12 @@ +jQuery -> + previousValue = '' + $('#planting_finished').on('click', -> + finished = $('#planting_finished_at') + if @checked + if previousValue.length > 0 + date = previousValue + finished.val(date) + else + previousValue = finished.val() + finished.val('') + ) \ No newline at end of file diff --git a/db/schema.rb b/db/schema.rb index 31f3b4487..617181402 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -64,6 +64,14 @@ ActiveRecord::Schema.define(:version => 20141002022459) do add_index "crops", ["name"], :name => "index_crops_on_name" add_index "crops", ["slug"], :name => "index_crops_on_slug", :unique => true + create_table "crops_posts", :id => false, :force => true do |t| + t.integer "crop_id" + t.integer "post_id" + end + + add_index "crops_posts", ["crop_id", "post_id"], :name => "index_crops_posts_on_crop_id_and_post_id" + add_index "crops_posts", ["crop_id"], :name => "index_crops_posts_on_crop_id" + create_table "forums", :force => true do |t| t.string "name", :null => false t.text "description", :null => false @@ -146,6 +154,7 @@ ActiveRecord::Schema.define(:version => 20141002022459) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" + t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index faa4438e8..a3edec52f 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -1,4 +1,4 @@ -require 'spec_helper' +require "spec_helper" feature "Planting a crop", :js => true do let(:member) { FactoryGirl.create(:member) } @@ -6,7 +6,7 @@ feature "Planting a crop", :js => true do background do login_as(member) - visit '/plantings/new' + visit "/plantings/new" end it_behaves_like "crop suggest", "planting", "crop" @@ -38,24 +38,44 @@ feature "Planting a crop", :js => true do expect(page).to have_content "maize" end - scenario "Marking a planting as finished", :js => true do + scenario "Marking a planting as finished" do fill_autocomplete "crop", :with => "m" select_from_autocomplete "maize" within "form#new_planting" do - fill_in "When?", :with => '2014-07-01' - check 'Mark as finished' - fill_in "Finished date", :with => '2014-08-30' + fill_in "When?", :with => "2014-07-01" + check "Mark as finished" + fill_in "Finished date", :with => "2014-08-30" + + # Trigger click instead of using Capybara"s uncheck + # because a date selection widget is overlapping + # the checkbox preventing interaction. + page.find("#planting_finished").trigger("click") + end + + # Javascript removes the finished at date when the + # planting is marked unfinished. + expect(page.find("#planting_finished_at").value).to eq("") + + within "form#new_planting" do + page.find("#planting_finished").trigger("click") + end + + # The finished at date was cached in Javascript in + # case the user clicks unfinished accidentally. + expect(page.find("#planting_finished_at").value).to eq("2014-08-30") + + within "form#new_planting" do click_button "Save" end expect(page).to have_content "Planting was successfully created" expect(page).to have_content "Finished: August 30, 2014" end - scenario "Marking a planting as finished without a date", :js => true do + scenario "Marking a planting as finished without a date" do fill_autocomplete "crop", :with => "m" select_from_autocomplete "maize" within "form#new_planting" do - check 'Mark as finished' + check "Mark as finished" click_button "Save" end expect(page).to have_content "Planting was successfully created" From aaf08469e65c17d4b9e61987a6447e8571bed052 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Sun, 12 Oct 2014 23:14:02 +1100 Subject: [PATCH 059/132] add date when planting is marked finished from planting show page --- app/assets/javascripts/append_date.js.coffee | 16 ++++++++++++++++ app/assets/javascripts/finish_planting.js.coffee | 2 +- app/views/plantings/show.html.haml | 12 ++++++------ 3 files changed, 23 insertions(+), 7 deletions(-) create mode 100644 app/assets/javascripts/append_date.js.coffee diff --git a/app/assets/javascripts/append_date.js.coffee b/app/assets/javascripts/append_date.js.coffee new file mode 100644 index 000000000..abccb9969 --- /dev/null +++ b/app/assets/javascripts/append_date.js.coffee @@ -0,0 +1,16 @@ +jQuery -> + + $('.append-date').datepicker({'format': 'yyyy-mm-dd'}) + + $('.append-date').click (e) -> + e.stopPropagation() + e.preventDefault() + + $('.append-date').one 'changeDate', -> + href = $(this).attr('href') + date = $(this).datepicker('getDate') + url = "#{href}&planting[finished_at]=#{date}" + + link = $("") + $('body').append(link) + $(link).click() diff --git a/app/assets/javascripts/finish_planting.js.coffee b/app/assets/javascripts/finish_planting.js.coffee index 26f30114b..de640311c 100644 --- a/app/assets/javascripts/finish_planting.js.coffee +++ b/app/assets/javascripts/finish_planting.js.coffee @@ -3,7 +3,7 @@ jQuery -> $('#planting_finished').on('click', -> finished = $('#planting_finished_at') if @checked - if previousValue.length > 0 + if previousValue.length date = previousValue finished.val(date) else diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index 366d089d7..cc3d58bae 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -42,12 +42,12 @@ - if can? :edit, @planting or can? :destroy, @planting %p - - if can? :edit, @planting - =link_to 'Edit', edit_planting_path(@planting), :class => 'btn btn-default btn-xs' - - if ! @planting.finished - = link_to "Mark as finished", planting_path(@planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs' - - if can? :destroy, @planting - =link_to 'Delete', @planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' + - if can? :edit, @planting + =link_to 'Edit', edit_planting_path(@planting), :class => 'btn btn-default btn-xs' + - if ! @planting.finished + = link_to "Mark as finished", planting_path(@planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs append-date' + - if can? :destroy, @planting + =link_to 'Delete', @planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' .col-md-6 = render :partial => "crops/index_card", :locals => { :crop => @planting.crop} From c405639f224b56e32489b5e332e7f95b735c7b24 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Mon, 13 Oct 2014 06:42:57 +1100 Subject: [PATCH 060/132] write test for marking a planting finished from show page --- screenshot.png | Bin 0 -> 81774 bytes spec/features/plantings/planting_a_crop_spec.rb | 14 ++++++++++++++ 2 files changed, 14 insertions(+) create mode 100644 screenshot.png diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..426d1fe4f1038d3bd8932829c02f6b01cdb2cbc2 GIT binary patch literal 81774 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%0AY$ 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

9=510=xV4bvgTV1@v*3QiL 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 literal 0 HcmV?d00001 diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index a3edec52f..d8fae5c64 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -3,6 +3,8 @@ require "spec_helper" feature "Planting a crop", :js => true do let(:member) { FactoryGirl.create(:member) } let!(:maize) { FactoryGirl.create(:maize) } + let(:garden) { FactoryGirl.create(:garden, owner: member) } + let(:planting) { FactoryGirl.create(:planting, garden: garden, planted_at: Date.parse("2013-3-10")) } background do login_as(member) @@ -82,5 +84,17 @@ feature "Planting a crop", :js => true do expect(page).to have_content "Finished: Yes (no date specified)" end + scenario "Marking a planting as finished from the show page" do + this_month = Date.today.strftime("%B") + this_year = Date.today.strftime("%Y") + visit planting_path(planting) + click_link "Mark as finished" + within "div.datepicker" do + expect(page).to have_content "#{this_month}" + page.find(".datepicker-days td.day", text: "21").click + end + expect(page).to have_content "Finished: #{this_month} 21, #{this_year}" + end + end From 3a46a5bd18eaa79ef5139ccb7965e737a82621ca Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Mon, 13 Oct 2014 06:48:46 +1100 Subject: [PATCH 061/132] add comments to javascripts --- app/assets/javascripts/append_date.js.coffee | 16 +++++++++++----- .../javascripts/finish_planting.js.coffee | 5 +++++ screenshot.png | Bin 81774 -> 0 bytes 3 files changed, 16 insertions(+), 5 deletions(-) delete mode 100644 screenshot.png diff --git a/app/assets/javascripts/append_date.js.coffee b/app/assets/javascripts/append_date.js.coffee index abccb9969..cf54f4654 100644 --- a/app/assets/javascripts/append_date.js.coffee +++ b/app/assets/javascripts/append_date.js.coffee @@ -1,12 +1,18 @@ -jQuery -> - - $('.append-date').datepicker({'format': 'yyyy-mm-dd'}) +# Displays datepicker to finished at date +# when marking a planting finished using a +# button. The button must have class 'append-date'. - $('.append-date').click (e) -> +jQuery -> + + el = $('.append-date') + + el.datepicker({'format': 'yyyy-mm-dd'}) + + el.click (e) -> e.stopPropagation() e.preventDefault() - $('.append-date').one 'changeDate', -> + el.one 'changeDate', -> href = $(this).attr('href') date = $(this).datepicker('getDate') url = "#{href}&planting[finished_at]=#{date}" diff --git a/app/assets/javascripts/finish_planting.js.coffee b/app/assets/javascripts/finish_planting.js.coffee index de640311c..29b435ecf 100644 --- a/app/assets/javascripts/finish_planting.js.coffee +++ b/app/assets/javascripts/finish_planting.js.coffee @@ -1,3 +1,8 @@ +# Clears the finished at date field when +# a planting is marked unfinished, and +# repopulates the field with a cached value +# marking unfinshed is undone. + jQuery -> previousValue = '' $('#planting_finished').on('click', -> diff --git a/screenshot.png b/screenshot.png deleted file mode 100644 index 426d1fe4f1038d3bd8932829c02f6b01cdb2cbc2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 81774 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%0AY$ z%s9qxmNiV~?ZlEAV|>%`%(3c(wj`AdEn_}~&CrU+VaSVZ{%7%6ZpaVWu!S!X9nO=? zdn^&(Hr%zSIcHa*uiS4;tJ~=^tq``q9rvbtt{Q2o;z)_DS4{9uao-$WBcitsC|Xqo;wJAGyJ%5OAA9ey91JyWVUY|={a zmCP$cB?9(qk9o;H`1*3RBIYLO-Ua!v{*b>k`z#A~_$XatX?h`GxhqJ}Q0>7%&L#ec zX_;unr@N1T7sgd*)Q#&Qudg7PXQ7%G&9)4?r&;gQmcO-nndao$`F-IP+aj&Jh@1zn z$t`5nRsH&p?2r7M?#{cAh-0?(FVr`6Vh$E7Kc;yiWcrF2eg@H~V==~AI)tM#OY{ew zg@wbiDJ8KmA6r{sBmYN1L!(aKpPd#b{;NlhLl;7FV{WT~{p-0ZH8u9%I-FI;mQr&$ zjw8e=o1s=!bk~8nTr)(zL{4Y%U2-7LoGr2KPfS@k_3LyR5{gR7)O=a8rsb}Vwz5iU zXvXX!J<_$El<(h0N0SNgoa;_~+YtDvz(1ivUnY6AOf#+6%|cQ(H}eJye!en`GV zVSu^7Cf^V4OdRt0j^9}Jkj{CK^-imOairu`jw zf47V5Hv@{WXKpRro^6=4TO!*CG&i<-c4qX-@GT1lV_jV@*(@b#Zh!0>|C3_ZAQLWk zh5b*btv_fDCOU)`0)JO^K2Z%Kg0LUe`0IpidYoO zQJxNu_mb7Rb5hTQXguHBX1qo|jLx1Ec4cw5hrEV*(UP}`oAcH;H1BW^yOkxxCKVod zF1^DT!KKj>7T7d5lfqCCweF}?VJ-b%@`A)@ehx?_uX^BfRZF@0NB(%7F_t0S^=iU; zrn&9;S&PlPNurOXyYFE~3yRk859}_Ly$-k44lwOJkl>*#N|h+f*|@W0>pAF`xm&T< zMdWBPm=)ssrF>O~(w>H{DDHb|d|~CwG9HVsHxoK%%t*H28n6e7CE7xZ1IlO zeA9E}e$};-6?`Y?uI<1_xrc(F8Hx=*PowqhaK|0ok5pU0RhE<*Q}Y4G*k&1Ngtu9=Ds5z^kPl1l^KO?k`~->q69JgM^Yzqc^w;C4QV z2Z!{ft_o~aDX&r_^Q7>!Vg}UioJ;WMf1&&ngVfw>+9!G{l#PDMR3@Q`) z<~0Q~;I`}L32dIngri4G`u(!z`}may4k!dIp(F{or?|z|_8F%xODpRi;mkT@KAaqf zZP34N$w0FhCqpDtdOGOvkkI|f?eiwT5>*ceo?Q8z?Dq_9-SaNtX1!(-Qk`7i|EBQ!<`21{ z-K4UyskL3XcMS8z<@<mT7(GYP4|b8M@G$%8gAWB7exoI;*clx5^$?L%@YY&Y78c zfrlWVq1Fudj7`{JnqR;$t8`O5C*gVkBL?4hr=8WYUj`xn_C`gb*A_eoCUQ|Yx@ z#Vr<0GmL57wfq6I7AkunS{Cm&tCQecp(E1n?EJ$o+Ut+EysCP1{Z7}boo7AvRT-mJ z@<^+x%U`nlg7VPysT4LMR6NKkW$>oIQPLNJT>z$rJt>NV*|yf!g28hsDkuC}b2Z#Qwg~NnZVKS48h)9L_znyJ!#hdNZyQE~ouS!qm7ZAmGor;t^VeD zO+k0%IJx*e3POIlE(D0sYQ8A5u3{@v(dybe16-xfG{h0}O4bL~FBX}m{DSKx)yzBC zKhcEA8~xOLq*df+J0`p6Xg>Vrriy|>V0r>WeeWnLuNEgO_M!=`B#&o58#99c{gS%s@+fV3G6cN)&z|V4=0)r-H?oBgYA3{{`X_3#6;n~?dr+?YCuRx^F+lmRF{2I zQwi7B*SFhQx)4m02c&vctgNi61$w?z?k%4qUzumsP${XalLV7<`PnUY-eYA|+Ny|) zBd@G-X998t_tvlbs_N=178Cu&Mf*DeqYPIOqovTfos^i!PqY3>go0V#GJK%FpNe+H z+k}%io+g3R;Q9J?d4bW z77|oM)2N4Fw#qlzfG{FNJwO zGV3?}YHydcI!-JWTXFaVYybJ?JpbFQEIKlBa-0~xdrEf@RQHnD#J_Wn%> zk=L)E+-CH+-uGRmc{b!S$JaKU59+F_)oyBnUN16%NeXnlF(#HU{qDrttF7c2&?cU5 zJ1-72cv3Dp3t>4qe74uBAyC;iVby`A7?5x9n)2j(?|FGT#l$kJ|CDvAnbN8e8m+>6 z<6Ty0Bx9b9h;k$y1jiw6;>*PrT8y!|9gG-%HfMi)3)2l+RFCE(^{;kDd(1B`>Vv9^ zdd=C8!sA=2KaFs5b!u`Kzka$U5==JJRzm>`@TwIo>F#IjlR>gNUoNMZ=a zT8w`rW|37^&JACH4VE$1oov6!~y+5 zFkEcGHD}v*&enaqEr;*oNHFyFjT>IkEXK7{QHmCc@HLv&E|J~Vn`+Q=2 zoJ>GK;2I7Nw52_emzTFbw#5edC-goF9+z9ly|Gp6j~!=Wmg>YKZ{CRDCv7Gr%BZvR z8U4XnGyXP)WWKl|I5^m8Hpnv>)NK(f+P85j&M2T;TF}D6qTU|#WsZ&)6ShlgqCFJ6 zfSmG`+gUzz5)=ZSXcTK^kpG0+FSlRzzE1E^ce!`yQ)p=7GZ}bv3|i&y;OvobPnDYm z&JV7S(Rdd^H?#2Aj0+% z{bpRrC?@>k;^M1qEJfOt6d6TNCzw20BEEci@l94VrZ_$Q+Q7icvVB^4ISFVWB=s0* z!T*`W|Mr447B!x~*#41nlnoDP>nySHE}c#&%VIp-Xkh<$Pz)nS#}* zeLd57Wk^dO_5n^?XM1}Cu%g2~B3le#?>g7FdAg5=(CB+IV zEpkdqsYCNdMn*|VNiRZq->f~4h>KFw(3s8m9)J5>YHeQZ1CX73?C*~pm9E5Ax8^KN zCSzd*UNNn$)q0uMk0+?@ua}vXB_xvET>|#b?c&(j+uqL3?uptCW}0L)?Pz_?&!1S@ zE~d<|RF95w;5I@1S$z>KWnE&IRGPIXlP~uj?+iXG{AIzcER=kUvF6A0^cF{BgM+ta zxC;=78-#?=ki#&&GN780mR1M<3`A=IQPFFl2AuS4&|FiK>|o=ZB$81_*x8u_kWo!7 zt)4PFs(aLnqSePn#>TTp+d4KUd!`14hTqcD)4xqKv9tSERB)Xf1n~obr>DZvO01rK zer|4hF~L*V&5aBi<>eF=uQ)n7+OLli^0*$Dm5N!6mkOBj5)lz`pDbw>onj#t+TXvc zsHkY&2kF6?{tN|C@p50F4c(=tMng=mtzpZ^$h>^HSC59rt-BTAWWPswbbhE%>b%>O zAb9xu#*OI^=sJV0dW_D_-Ym7o=H}V6<6Tin$(MS6q;3IW>0wxHOUrflQ(;|T-!An~ zOYZFK*^JZC(G?iB`HP!cT5bSY@!gAWzu-ArZ~qa}tayV1=k%4KAvz@`<Djc~6mfz>*76c&R!T|qy+ow*ik2M0o}lH6uOrXX7h%EP7oRX!6@BCKgQ&`ZuQ$s@(g1D_Se z?ffBSohW(pG~pJwo9EWk2+ZEz-T%-IVHkl2nbm6$EOxva&Sj%?GL%WJgtr5 za%AwsAx*QnFphV=?RmD1VQXs}-rE(&m1PtDF+2MSw0L8Ke-{-KYnrHVkg~0;th{mK z2Ey|a0p?S9o>i{v)&^cvr_V4eHUhMJ3R~}z)LdS;0*{X-HHeG@6ZDB_BQrv51t#{7 z;44Yu6F^$|LDV5=sws#kaM&T+0qIH|p@)*<;q@tX6xU2{54K%YwGfX=;Go6zC)+La^h56 zi8q#n&jU5yb%P_KW~jekw=XRSY4IM2U}E5qyo=OU$2K=NkLYV?)B-VRI*4ZSY-50c zR(WwE&wXZ{B43`D$H-=>xKWt4e1Ym!WIiQmZJodL!}DkcXdkXD2RPqM(66 zN~gYP{Xv_0bpOJqt~N_a_id}#zscLU{n6^N6FRyhYRo) z!@fefwYBvIF^5Qg0-UDWnv4BBqaqQdqRYr9&gfQ^1)A&mAK;NVfm&Ekjz;?2ycuV% z*LEAd`$Y10>9{1@i?ox&%@D)3FKFEx@s_`BB_>=zu&cmqK>QZrb#u1<*k`6sDLl_? z+P~6dzc_Cs;wr@b0J7a@QG!VNzpkHibbex{9^$|CH^XARbQgTj` z04wMjPm$Yq z_0U$O$e=(u?_IvC*YX)cq|nK-MK}corhUR-F{EhR_m+B83zviAoJBU^3%AebrsxxXv97VJUaKDDbLe&_H2#A>LxS!f`WqKO0}A*zAxe7x*ai& z=;-O?%QYuvkAV2LH5Vqc!pdkqRP7IHJsELn`S~}js*m}esPEl_j&*v@jyNvItwD_} zYlv6GRws(giJ(8Zz@#TIAAa)8GJSSZ)xv@vL>jUVj1&_hJ7U@E$Hp+gi1*X-b-Zdn zBc4^q*z=@dHOyW&YgNqSVkTnH{1wBxnK_Q3rWWn}3=eNWM>Op7=PRZZ6cpl&m-6Ji zZg(g>O4V$=^3&2t;2kk5>p;{~!_2|ep9KYP6B78KsV$hlBA zT8Tu-hl1dKPxhA4`1tsMdp~ouJ?ky~|c>dU+vI_aM-GJz1%?S6f$ChYHr} zcYFKmfOK|?sZmWGx9zglNKYLdo!|4ekO*NN4AlBeK{|xPV$|NPp-ocr4*W%JVVs-Dgq$#JP-<*fqWvru<#|Q*!BE0X53pFA*ijb zR|#2W=;zP(me3M2GchUM@^K-&_J`N#yf{a0d9O%W>ZYsx>@FP*jadE!FE207ki~cD zc(cd>MF~xRy{9ZJ1vaz7J=Iimwj8LasG(tDeOpNk`#wImGX3(`CMrrM2YdRB6Wr5w z=BL`)u!@Q>dwMuvr!>O91%v++p5>IBxzNRQX1XHC*X88u`mMa&vV!I5Q*?LV((-cJ zh(p*1kdQs*JuwOFy`X1cXiAk}4hRUikqvQ*owxdJ=)|_RvkUtwW0s)qj!4ulvHa!) ztZ#fWvXA5An$QS>I#yyu5U2bw?A66tvkjQ-`jHXh604~u5fEZf{6<4VQwt~g3ZD03 z=k=P;JRsu*0;E6WN5Nzj7__u`7xO`!=uK1$9Sc~P7!YEE!PbKi8;-fxi*Nk$@;8<9 zJhHyINAto^j#)TO-g}_4Jp&o4Mf!`FgoF?{`LpfOed2xY=lc|pG)v_Pia0wtO&1Ma zyoZoa^R_8KTlHD&N7^(A`8aWaZ zLjv1IMW2O~rn#srM!|CVl;Zs2bd}_Sl9-rA^&l&+`l_d=XI$~m2W>e`Z#F0CosJ>7 zfJgqF$z^ZJ&-@`PE1~&dCEX~4x{98~CCZ&OA&3j5_sUFrQ^Y`<2KQ@R+-188!<+R{ zUuasUlL8^uSI~GbZZWpe^j~|Cr>kV)ck6M(JRs7WTS`nXx-YtZRiVCzERDPq62@J%7Kp^)AW;-ZUAXQIFOG}ZVG$fyG zU+9V46U|8ky4ulYe8QnDwJF|_NdtPdjFwnIECGY5t`dwXww z<@3Y^Aj1>9Ef&sO6dFjh3IP1Lf{xC51^4yq*Gz`3inTWAUi!@*${Y-?3MbEV^H8e1ohO`(zh41ogirKXHe(H~wp&=qVwER-h0lw3-3Mfe-surK=Y}tx%v0KJX6w?g*)t)m94P{unDi6mZzm%b=(*?J>ufz zRL|!#Zjbu*#@^ICtWg{dRn=)wh3n4{R_HUr;GZx- zMPigLhomin^z~jw(S6iLth-G(BB(E1QCl@PUasJhvM~~^fkXZ6eL}@lafymPrs@(^jo}X;#EbAsC@A8>Ig2tg zGK2w-62!SCNam=dSfuo&#Q=OB5jT^lvsd>c{e`>-dB|`gvvBTBhTeyvI9eET&{O7O z5AZ7VmGd+-W4Ri?vA1u4M8ZW-N!DTjk7J;Jd$!r;?8wG`rT;qk&}-M1{a_E)H#duX z-%p2WE^k4m0ort@LGa|c=LMPNWMu|3LQO#-2#k<#szhXxmCh@rzNUVh8NfxE^ct?M zjU;owcp>bUuiumwW2edbzdxgFQ!gxpA6|`)1(oq|{><%hgK!yUhbo{pg@60@L#Ou@?p)|IGb>{evH$D&D5>}bH%wOP(4{7D?sGxp*SF=^ zw@`{;a(TveNo_j%67g$7t1?JM%b%jPfB&kvscGtfOIBv)4RZ3Jqa&yL4<`XAATD*> zjBlE>iWQe3;?*=UD;X?E_Q)(sO`F6|Sw1z&zyT*O9mkmwC;a4j&yj?It?h^DLx}!f zu2$Z<6@a&!k~f?Cq1^wPhzN#2jH|Mp zf8;#6b9m@qQ^U8=9_<57keX$-=tPXdv*TrUv^5*bDk}QB3mwRmj8g@f__j3)~^3x=b%L!%|(k{>oGXRBj+p6X43Fe-+N{xj4Y) zhllBCb;JRgXI1VZs_+*j-jN!c%?w%y*J!E~G0!q=%c%+2-=1C@7npT}6x`)HB0ZDi z^XJbWJM6Cmh?VM;8dt6d+R?roM`XERFy@Rgh4;Mz}=2(o3 zRuBeKJ$XVlGBN_*HO?&Z(Lr}|#(7*nZp1TuQX+Lqc6}slD;|~hGIU{XZzyP>r$?-t zIzK2VXwcWWZ?baVeO!rUA9cKc_dBH@XNcq{8XZTy!t3%zhK8>oVN23^Tvb__@3gIF zzfPO;$T9hVcD?F1*iYdiL9T&vtggL%@^9$Obphhr+TB$s?|DYfEAi3ae{1GNHv4kZ-oR?Ls+8e% zMJaBornX}K3pDuvVisjwEh#P*?uwr&b=ppncY#U^GWCYdZ>A}1VUc4({|)fBRCD=k zvA5yU1a@}5^P$YKn#F9XsmRow@SL8n#*jRhK$l?{zJZ_2mj$KmxcgzM6PC8 zb}`TC#rbK0ZY_cM<=)81r}Bbq00+_Q;}NnX(xPjAw1Y-6q<+sg3`VhSD59S#RBpsj z^YKwa*RHvDq$e0?B5fy>z}r>nhmn^6w$;`k1EM>i`KImk3cuLu@=D+Lt{d$-nwn44 zt5gZ$SNCB2fr6;GnYV#H^##*J+0PT^GDfZP^n*h#BfhW7P~o~%e)i#r7iSD}pPX$elcDT1zU z@tUB6%Sa5ANCU2-DX~^%XJjy|2ayW?`1!Nm3k5B_7oNkcTtANCFS zpB}~;$VrDJJP_&GMtF$D&Nu=Kl`-&FvZ2;G6nq|<>uB38VUM=EJgWz{Hur}vQx$)a z{3#7HWr0w=RdT!BJQ#{7|nt#Nx8n}_iv6Qr3FY&wvT2* z0tZWxkh@u&-g>OnWaWFPczv^(Jn6hf3XG_wUZG0kH5aIZDj#crgf?8rBp)g&r+Irw z#@_>8jtdRx=%&ar^8V_OQYZDB3swMSaT*H zY6)W;oT|6?_TEKA*c!h9z{(7b!npG*;U1Nhg+)hR@jZj5(I$P@3hrj1K#anYvy&XC z8Wx%+9C8~sxy&)oHiuC{LIRtVP0{K&>ENvtqR_yS;iGuw$`TF3M!89Y%9=2@`QVdm z?J9K|dUkdqs4NTTX;9UaXKlE4RI)gV}CD5{deYFO#yp_5#>(`r9Jx zfGcvUuCA`{{D7oApycMpjRd_wx=o!XCLt;+0EcuCD-90^N2od2(?Rw&eveGIGmIGJ zMkbxVe~Z=g2x1J5EG3V`Xz$Szh;24$P$BqQ&3QY#oi6qgHQ&urGB^37ZU zyCtas``v_keR?|4Az~T=yjf80 z^4>A*isvbX=4PXIryE)oZqb&JRh=0T*zF30upsW=8s29+r=IPX+zy|Q<75eihSHIp5$J?+gnx*+Z=K-q_0|SG%5h185G(hGO zE-VS%_l;04o9rFwi&5iR zx}QE-3a8M?#pT|;d#JW$WZ<9zf`S_1jK9=5At_nRV28Nheq)?iOiWCCIW_gcbGwDY z#Ckla#fd46rbIy*1@>x&;(lRa;g!6q16ugDz<8c|Ic4SGwn+V#%_UH2Nt?9N^T)gN zdiTx=B2^5ybkE|rbHN+)5q}YjE2drCyU04fSVrO*xbx?(2iR~u(=$EGU8J73o<4nt zXAUKN`IE8#+8D-WurZL-f*x}wUS93)jL)1_)nzZT5d1KH=894c--!tpm>4F@@#jd$ z;hXF<41*gkUe~Yn(KIn}%yG5B#oK%jb^i~zJWf!HMB+#Qu|YcF)i)2PD1QxP6)cVXe8lOn^Ad!37_=)NFY)6*_R5J2i0Y$;UKz}h2_O;^6A_n3 z&2&pJJ;G)0nxM8eQvMa6=zF;lzuoia&vgNOEATkS@SXxKO{k#pOD{35TnS)9OX93@J*)*#Eut5G zz&}#k1s{n4+AIeY7IKw(oR=>>CQlQ7eP@DBC0BQ{vTSL%047e-Zf$yAiexC0%d+3B zh1fjb^U@=t4J!NLle-cYlrTYaWhjrY`gnmf)xQl8l*;3UORC5=sHJ@?V4|hLKX9Q7 zYXOD@E$}Y50)j6D4RULc+XEtcV3?dql?0g}5oW++xB(!`?Dxy5tkeV{;{qrsnhls2 zJ%swnV@5_OcH8PmmHz&|GfA{fY(NGG2c3|oT7G?(^h_z^%3NY2jaHwrAeAZDP#B;i4_c{12AisbQS_1|?({LT6YC{mhf zre(1F>jGW(uGNF)BvKP6b??WIAAY)u`fyC(E?lf0vc6eUh1~`6&WEhHBUvASxp14_ zMb3~P>iyW@Q@FKV*I83Z7=iMD%pE%ui1OqR{R5Ryl&^_>{XH@b8GjwYFxV^Sw6{Rv zbdc;i?uK_Pdr;qY+O14ZLvT74cdiR6?dHiP3uj>?;JXqXH+fH5?ibPJaNW1M0*?jm zFnGzTsHjMs@h!N1IFM<&<=NSiJ;&>7Yg@5Uy(+rZJ-*ygZAIQf@;NQZw(Nyx%=2Otc--Ct!lnc(}R$dXa6TYPI91mw zfw@djm~`kU9~?tNQUpMu;o;}CFStC{gSFc^e;!4)G28rk2@^H&Y|DtD=Aw~$%?1#T-rnADm+o8u zC7B#IxRI6~+;geT>{VOaANaR!y@MFDzCC**^u2@S!1%TS8Y(K}MCQn$&15_$1hAQ2 zX7#*TC-FYsU8H^@SyCq%1{(gg_4R06Z`|I(C=LM%GBUEQ1_G@6JlVk{te9}#o1pM# zb8P!@De@`{3kx==<09+807@$|wEee7QVjcj;GoIE1ehc+Ow3caApq-WyO|Q7{M^5l zk&B$u{uiVNOde79_^o-GVGdmc6Fw%9z6o84;kP29zie}$CGsV$*AaPj&jFM+1**=WAA_vhr z1mBtUH4q^7{-FS{aeXoYa9BRJgnch8Gq001>WUAVo~FvyD3NB`E0lcb1KA|dKpJ2i zbBI-S&y=X5pDt{ANIvu-mwN87;%H!Gb|{c;+!dd+$mn|cNUP-ortJtzbg-FOrp>DI zO~^^WO=AFl4JDZRzCHzu?-VE2vrRZ4@}ieo2wJv(QX_9?MG3k>W*{mpEi5kn6p}xH zvCw5?@@}v9XaD$dv&8B<6$b}8Eb7u|5go7=?5p1bsC|!v<2ra(YkPa|iV9Nr=Ye3) z$#CWnuIQ`okS?{~%BvX61n&Vq1FS&-OG}C;^z_qzdhy{@FO72~&iKkrK`sQ6MQ`DF zLnZPq9Ua!-;2@k>g?c_I#uT`ufRDEZ-N4Z~)^GU$%TQEYybc`1%*rVK-MbU&Qq7Tm z{WlLyEQ0NAq6 zKB{SGX+aoz6CWJYRv^&DsHm4gaq<}xQCTp){ z>6#H<*NJ&to*`j}?Pd)j*G`L8khFx!<-iU4_fsg_@Tn$bGznQQG8Hv0qR&+5m3WKzD800$G30UIIwm9-&nu!tMA==8pB3_5S5Uq zhbY2&V_Xd=X_y|4*4ADnC3r~C2F~mY2CK0MO7KznFf>fy;C1E}y5h47v{2H?8a$aMpXJpmuesYLTKGud7M zrwRG`J*>WOyagZ0Rxd)zpvs@_O!Y%90k+Z%Qkxd9GU~ry^pre@4G|h1{!S=R`xk%* z_S#Lr)~u~|cioSo`}bP<`e|bkz?ynJoKFGRR#HuDY%KNT$JYQz0=!b##f38|>4G0@ zn(ipX7i4jq4s}SY8fWqE=aWq%P`q?Jj?vjI$8V&x89aRgwX@k~880vny;hBljX+=O zvH>BZRC}PWz*r}y|IqR+)U(V{ft2cp%!QF@XYd0F?OJfI=!};LT`Q{)BufiPTR4L- z*$;@fIXiM%T0y`i4v&m9XH8H9Kqft-XZ)289GnkR;K;{}yoANZ`m3Bb6_uZNj}cKNm&2tW^!T~URurazv~lLFWWvK%@vEHRk! zlJL7sf-vk<2-ZlnwL3aGW`Lts?M~edfy>^;goeHSTVRsD9@+UO4^pCB_o%5+kcM=) z!jX|d^LdE3H!5%f^qPYi(c4;DTkGIVSfA|CfW?B$;1_t@iE;mmDq zmzQF(D?vn0@7_%UD_~k90LufEb7_nn6}SWfAr!A@7gb7vpwYU&24&d(H~qnH4~HUVkOiN;PuHYhZX^yPRNpoKso#vurC=+iE(jpqc|N%fZ!O> z+vtsnfYCxeQn74*AWTv|E-D9dCfo&_(q>>}0L59rX*snKHo|Smp=Xy+)CS5Sl2^I~ zbG2yDAA*)O`sxSB-ZGWwfG?R69uGh+P_EBld=`?A4k%)-%cB55Q|drn^DUfl<@`9f z2N>anq%|Y|Ab%m0ukVq}$rc4cXi*DO0U_x~-qQdxo>1%e@(jL8HD4Fyp|l%+)_?BJ z2<|NuP(D~@fs^yY5HD~fbMx&9d^)}Mz(4{Bw8Z9MO|7Nvsu!ejRCKGX$T;nDS`85H zfTOOW?{M@Vo=x^=&%Zny(az2PomkYjIa-7TYk}lOsj8})e65C600%-?dBzT3;rqX6 zMd9H@e-FdwG$p{;bZQu4CV^4vlf4O&-}seha43M~m?)FL3qB2*p9NF$1;eDY)aC!h zmnm9>9HsdSJ*Ao3aTjoR;bF-sC{Q0sF+@^RQ6V@uIB-xtr>8%@uAzEpdjUXqK=m0^ z6YKi=2!H_TO>z^q1wW)X&^N#uv#45tGJ7)mf2r^353-FEsHN^Rl)M4m9y{V(!FMqjeXo!bnW*ML8S;0FRP(6y-V zbTZS@VnRr3ZEO3vQ1kDi_GZJPf+Vd_pO%)EQYUD|MS?d?xKDqnbLm13s0z9)(Q6aV z{zNc1COT*k_bdVXWm<@ngF}odhZcC(sxWl)E9ANhINfM1&3a=uMq*t#Q~;XVKqZDM6>E*;^gLgA&v;y(s-L@Y*(K;#bErcz8xMBF82BZ`b*sZc^YIkES_4Si&<3ZnT zZec-O-D4K_2)KR)7GrNxr#jl(6^osTPX91C1aLv5ovnS&c6NR)jCV0e^D z!eRSvbP6^eo#^h~o+1~L{FR-PgRXN#Dj_Y6pW+S>omnJZS|XLs?6VcrE5X6I-p>j; zaY6GQUZRdjTew08dizhI2L9+|tkgyypMoMc9Qi9UQp5( z0T)z#V)lJ6$n>SczB4=)0d*Ujf~I+Jgqi0PP*`I6;f*XTG0p+c;bxlrgeDY?kuU5# zI4rO(z&LvjfKhrlvc?H1^eZU2eg0y1yki*Dr&($(us81sL@JJUl$SJrIDfJWOZ)w<|42Co9YKd{}>1sAkV< zs#+_dbr63ygxR^e76KW00`cHRim}0XBVAslH!uhyc9ArGAeMgVuXlS?;~|30xuU>pL!TO%;2=$-qr?R_(TB4#+5r@!Qc&cRW-F)0J}sbE8he4 zRzQBt6T!qzq}*2HC{sD=4)L01*}ZJ$MDy=nQs@|t0sv&A>+GxKYQANk(!9GwW&;2) z;Flkp=7nGGAt{sKcS(EUFIXf7n0k(B#?jEwfaGgGrlBzv1)v-7=yz2||GIEf2(FMp za(lw%^TE7J{w;8d=+LGCFy2761F&vjVxKUL#o>n5*4`e(I#+-UhzyiTNnx5~$@}Ux z2^=<2e4sPmKsygZw#z37HVCn@= zRCLxGfWQme83ivy8u;)6ii${3v>qQ4_AF;o;lFVeeUj2u(5-=DdL$Y{*@d@5E5n4^wImN~M59{)vLRZHG0U5F@ zho%ae!XWZ$0#q)d7b?taYirMxyYR5F8vw^G0O&Av51_jH4<2O19Rkr$2;5iEFy`OQ z^Yn}I5C*?K#6GGu9%w+s&Kqz;VoDN>^uoYrZ#4y+kO{QEd#1p&MQC5d#l<0O47K*a z`c1Z=WES{AWrC47lEIiVz508w9b+(O{?SjT8jWZIuhS$?vWev2% z_^>bnFvh{a6TJ?dSWOTY2z`gz%oaEnm}2!oz~?F&!rRAZ8wQqUX&z9AaHF!bvje$9 z!~Pu#&|)B((wnSw4sGi|j#N#82yEsMLMTXTYsbbaqBV|(*dVTaqW}B6`hOss3r?0ZI$t7FXGq(Bx3vjDE5~b# z_3l&&BKzf=*cuwkI;EspP*cm%5ChKcONdv1m&{-=NSet+gE zbm0AcnIfC<{&|^U{Gaf)Jtz*vLUBMjG4X_=mGA*bxi)p;{u5c_7YRaiuKbkCp2^YQ zcf5CaHup*dM=HRzYu@cb=1O` zk|pmIS0?tK-eu7^Iv0Q4)b9X;%dSaxe-Wj5Pcl!LmK1hWF{*Fx-btgr3--q0I`Dfu zJ*pz`Qe~}_HfvQzs2~Sjkzb$ytu^x&dw_ik#tShYev)O2&(X){_{8t;M~m?b{$AYY z=9PJRIBr+H*y+x+=-S0`ShAf#bX=UBhTCM(VRT<><8iIQ-s=^nzgy%bf+?1}U{pMY z6T5^p_KH{iOm~>)-+SQjy_4VfM0o(M<1xyA9$L`%D_(zXsVG+B|8Z|c$;DeGVmYt; zbN!#f(u5u$9=0E0=qFUvA5W1pN=@@kwHu6GkRue8m7X4yaXk-rJxVDop?>hG3ghsc z#-S}?QCIpZUJX~obry>I^^J!PYlkkG?#&?`XOFUq^K7!#3JzCvdgA|CfwTQ~De_-B zZM6f`48~4tOo$KSfy2?|v5`cTO%k4-!TeKRVWaAGgGyJ9uihlU4;nEr>SgtOG|ab@ zQhfK()CV1cfY$`IHJeT(KDFKbGtXk`QKqf`Z{MJqgS(n?I)x z5+nJp1dELcTFI}?j7TWYk7!!_<(Wx>_JZEeA4oLmZ|BP@syEauR@KEtPSkzfTnzi- z;3YjnUb^~21AbGTL_()-SM7P!$A@oH*8U`vaJQ<^5%tkjQb#C7?8jrl1l!g%%7p@I z4tzdJK1cz029%o>hIWOQEr93yt$U`1q0jsL1sn%X`Z_TSpxgQ4nM z?O%!RIM$lLg=UuMASBpxr^i^Q!gMtG$3mGbpD&V}ylftH;aR11-8GL^qK1I!^ zqkr$4RjvPjQTG;ZQLa(@=pYKBgor_-AfR+Nj6sN$igZdNokN47f=Y-;mxP3*(%lV% z(%p#E&|PPZd++ae&Uc+Z;GBJ3UR#NodEfVW*0a{V)_vb?fxBwE-;)ALksYS+j0S0( zr^~nIQbPu2Ru)$J*Wb5YkkBpjQIL&U-`+8X`z=jW*7S8emc`Tl}F9)PQzA8Wfes^!+($J5M`wg znr9xz^maIIyx&~t%a94|q9MlT^ga;DKJs4@toSKs+0=N-_SrS^Fut z#Hi(*eA(~S&z~dK7T$9rguH7?3v9^l$xE7YAZIR96XTm}EWqk46T3b+cP`fLf^CI)BTDw;BcX#gXP^ZUt$H8OX%EB!m33MFF z^`vO^*qTWjwMFxbZ5A{Cp`vNF(5q6AGH$e6=~vq^qg!z<&-c)fqQH0HiLI{Q7@~sa z;&e{C&0LXOQ;4HJXMT0U>dyK=wTIv6D~3_;6>)7;f7nr;7X>m)?>A4;K~NkO@)dF7 z%ED+7Sp-peL$+5h(>E(-)uZUgGx@KxGyh!bpDOv0Gn{woyjoGdhl^hF7EqR+P;$xS zI@$guH+yvscWs)Pw}oA-t|1dM4ey$NG;o%fuJx?;M7wu|nvRZ^+REQB*^-gb*ojcj zcM!+_k%_PBx5+aU7cMCo;|y1t^5a{#MowOVKZd5fw0_#LK zu54y?M#(59b8TlUX;d>P&CfK@d5LwYtt!CCIc;^SkAU-M8}3M{y?8AlmLtG#ID{)x)0yBks_I#6orT2CXJI7j&14=WcB}mQD~du(~VnuTz! zs%a{IB5@*Fywrj2)!B2|hmg|bU&6cR)@OjY+~eHC$%M}K+FtJU{pfS)WIdsIVT3Vg z{E0Zl&?)tN!J%QFT7O@SFgtHt(akAzt@z#5^!Tly#fAR$&-Lb!3MLW<>&o-kcwm3Q z{?2WVEWx6;Ti?HXRCxTEDCAs1S<#p2T#5N4KsHi(-D}({Fo$=ixJuM}bu<+2*Q;Y+ zbE?a_hTiXfP71unE5;eEjtI7Qvg*kv?jQLluE$1eYM+@57=M7>n;h+7s-M?$OF0d= z?s`O25n_b%`iZ7Gnws?L%Fu9?8lA9wS)I8W?$~dXrD$D&|IT#OJwl-uBsVFwYef*Qa$c)(Ae>-D_=R!yk>T%F+GUbcjV36TEm}qI*rHh@~TSal{sNKzr@@$7_6>$Uhq{ zBOh%M*ytSacG2rf_?7jgarV8#gFY%Pj|d6t-Cve`eIvmK4bB*aH-)cs%wOKr+lsO3(J@lfhjw8Ko2`*VSqeflXXOJ`A6seO-9 z`*0^H!d7j^N_KUN(IJX(jeq+v-5|41ZssE!y*`kc6yfniDccS0E zr(Tlq=1vkfQN@8>r@KTl?>7*GKWwP9cB~CuM!6ueOwTOT9~mb+G0seCQ*Ep;PI!K7 zH9>&F%o(n#{9Pgly=?6U5~}cWr?|gK;%sgr1;UAZ)DVHRQq->t!2cd8{%2I+J)Dho z14zy!?|~}pp`#&_5UFnBDH@~aoMqhqz@}`h2y&jbKZjBKE0!2nW1g2)IHQgyL>23E zqLK2cB3(;VWX<1JG8f4EX??6$%JYm?g16r7A7Ay#Ip8lCxB=T~^XG`tdmGXd2a9kRbQ9HJs>ljQtLX8Cn zRO`#f8IM{Sf^wYL5&GHu)-g9jZkoH_V&zTR5-<8w4WUp`y($vl+L}CFU-N;oY^@1b z?fG$jOrLOYmDXw@e@w@A5q=(%op?6x{n&|!zxg7c_=6|4q`TMn1ix=>=(xBS9mtJ1 zPTr_aN7yLY%s-p2`;+YUg5@Ur zG`;>utZ%+bRg1^%WM#5M7-w3lT!{#~G>baw*-nNebbWoJ$tX*_bZXH>*7Xjp2?<-F z*z;UYh?2kJ;E=xK$!X(pOw7x4q*u9wS+YnjND3&|tqNAd91Gs_dU@sZm2Q?BCH)jP zrM@=0xT(j=$ppPvuGF3=%+CG0V?x4{vZl|kRF(-kkz4;F^l>BS>K$C5-s{0Xl@UK( zZ`ZbK`Wo9eFMr|Dg~;RMX2K=@z1R4sle*PxwBeLXolDAk8u_iu_CGzg`WyqrF-vnj z>4TKrGN=Q)tRL@rh<_X9O{Q@QbDmg6_0ge%dbgohXQ; z%TRwbm(Yj*6rPyw%ZPt!oe=34igJjpYybW8+FgSGPyO}F&@BWZ9`2;s|Eb$-^_zVX zGwN-%@?{Y&lpYyBRQR08|G6%D#Gs{Wg}j#n&D)V^g)=u=s2_!8$FgV%6E7>!1pQOL z6+9+hMz5QHELBkUn-PGe8DhelTh(pNI1&D)PxX~fJI+4^VhRB{*QD_VL(3>$er4=_&WXOtJCn99Tk&b<&a(;t}FKQ1%12 zx{U~1A!_s~*aog|uD=kF{pU`l+-qaaf}aH^dgd-{MB)zdsQIKLM|tb@i$O&8LyiSe3I7JUgKpGd=@8O71B6zmK{_Z_%v#(Co|? zVXHSiV%J!Dld`e`0AFQ^eo`bQDLLzC$M?6m@07_uM52IV8_G1+{O6Y-mwC76sBQhW zd=$?cpqTJEnRQA>6@Dhj&tWJQI!x)0(TeTNn&af%?s0cH$@Eh%vPyK&kylPHP~myr z?__k%h);viJ7DJ7y9@yZCq1^&HEsgYQ}?GMuR~9dSJ8vg_Nv_pPm3Qqq}(~h-c_Z1 z%`;e4B`m=tEsYsQ&Tj&F8X6l|wjq;JRRZYnT&AKL=zU)FknyTi#g9J((oH(GU)Pgc zSzD)OUKgkTP_uiUkQsA}DgRf;%KWkK-GoUdbG^}O&glAr41QVG^J@h3ECrbucI<{X z565l`LBR+LUfo8MOoQQqQwFBg51WINwGUh{rQSf-@7kR5%XeMME*7%A5hsc%3AG+8 z10fcZ-{?5GG{q+%@q^<7JLc9-Nl;2AZ{oT;w2$a?7x3^jT$w zZ(m!iumcD<@q<$l_zn4qu2x(GF8t2wM35EJvd z(IDej#Pr5}UA+x9L89uIF4gL;sUzP8C-NGndh3}`mDL{%$7)9vN2Sg!6My<7Xb~1F z=k&6UKW1kvt#5A`+D7CHBQb5#1NgRFf>FB>O3b{cH%M>Bm6)u#h!g}l59#XK5wEhl zzpK_;yS=Ga`Jq}3@t`dJfujov8skKOn7T*m80^nbe0!;@geJC1SMa)TZOc_$8+)%J z$C3MxewSpOnuQ-72(M_Nn%e8j$0gW@x#(i*o5FBy@br~8I6bQOSuxdH<`wJ`Le#{m zoL;Y%-i|L;NH>dojOn;;A#sb}lup_=WSr)J_jgA*

9=510=xV4bvgTV1@v*3QiL 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 From 6bcb9dae4abf41aaf61fc239abe0ef942f89f528 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Wed, 15 Oct 2014 06:41:28 +1100 Subject: [PATCH 062/132] trigger datepicker when planting finished checkbox is ticked --- Gemfile | 5 ++++- app/assets/javascripts/finish_planting.js.coffee | 6 ++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 93fd9858d..d789f6f0e 100644 --- a/Gemfile +++ b/Gemfile @@ -83,6 +83,10 @@ group :development do # Installation of the debugger gem fails on Travis CI, # so we don't use it in the test environment gem 'debugger' + # A debugger and irb alternative. Pry doesn't play nice + # with unicorn, so start a Webrick server when debugging + # with Pry + gem 'pry' gem 'better_errors' gem 'binding_of_caller' gem 'letter_opener' @@ -119,7 +123,6 @@ gem 'omniauth-flickr', '>= 0.0.15' gem 'rake', '>= 10.0.0' group :development, :test do - gem 'pry' gem 'haml-rails' # HTML templating language gem 'rspec-rails', '~> 2.12.1' # unit testing framework gem 'database_cleaner', '~> 1.3.0' diff --git a/app/assets/javascripts/finish_planting.js.coffee b/app/assets/javascripts/finish_planting.js.coffee index 29b435ecf..b8139b7fe 100644 --- a/app/assets/javascripts/finish_planting.js.coffee +++ b/app/assets/javascripts/finish_planting.js.coffee @@ -1,7 +1,7 @@ # Clears the finished at date field when # a planting is marked unfinished, and # repopulates the field with a cached value -# marking unfinshed is undone. +# marking unfinished is undone. jQuery -> previousValue = '' @@ -10,7 +10,9 @@ jQuery -> if @checked if previousValue.length date = previousValue - finished.val(date) + finished.val(date) + else + finished.trigger('focus') else previousValue = finished.val() finished.val('') From 25cb3fd469426b7f9b9023922cf4e4c2163f8528 Mon Sep 17 00:00:00 2001 From: Skud Date: Wed, 15 Oct 2014 13:58:21 +0100 Subject: [PATCH 063/132] Fixed scientific name upload bug and wrote tests Wrote unit tests and refactored Crop.create_from_csv The actual bug (now fixed) is that if you didn't specify a SN in the CSV, it would try and pick it up from the parent crop, but then the testing for "is this a dup?" was based on what was in the CSV and didn't take that parent SN into account. This is now fixed. --- app/models/crop.rb | 71 +++++++++++++------------ db/seeds.rb | 2 +- spec/factories/member.rb | 5 ++ spec/models/crop_spec.rb | 110 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 153 insertions(+), 35 deletions(-) diff --git a/app/models/crop.rb b/app/models/crop.rb index 3efe8be57..e4225f7b4 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -127,52 +127,55 @@ class Crop < ActiveRecord::Base # - en_wikipedia_url (required) # - parent (name, optional) - def Crop.create_from_csv(row, definitely_new=false) + def Crop.create_from_csv(row) name,scientific_name,en_wikipedia_url,parent = row - @cropbot = Member.find_by_login_name('cropbot') - raise "cropbot account not found: run rake db:seed" unless @cropbot + cropbot = Member.find_by_login_name('cropbot') + raise "cropbot account not found: run rake db:seed" unless cropbot + + crop = Crop.find_or_create_by_name(name) + crop.update_attributes( + :en_wikipedia_url => en_wikipedia_url, + :creator_id => cropbot.id + ) - if definitely_new then - @crop = Crop.create( - :name => name, - :en_wikipedia_url => en_wikipedia_url, - :creator_id => @cropbot.id - ) - else - @crop = Crop.find_or_create_by_name(name) - @crop.update_attributes( - :en_wikipedia_url => en_wikipedia_url, - :creator_id => @cropbot.id - ) - end if parent - @parent = Crop.find_by_name(parent) - if @parent - @crop.update_attributes(:parent_id => @parent.id) + parent = Crop.find_by_name(parent) + if parent + crop.update_attributes(:parent_id => parent.id) else logger.warn("Warning: parent crop #{parent} not found") end end - unless @crop.scientific_names.exists?(:scientific_name => scientific_name) - @sn = '' - if scientific_name - @sn = scientific_name - elsif @crop.parent - @sn = @crop.parent.scientific_names.first.scientific_name - end + crop.add_scientific_name_from_csv(scientific_name) - if @sn - @crop.scientific_names.create( - :scientific_name => @sn, - :creator_id => @cropbot.id - ) - else - logger.warn("Warning: no scientific name (not even on parent crop) for #{@crop}") - end + end + def add_scientific_name_from_csv(scientific_name) + name_to_add = nil + if ! scientific_name.blank? # i.e. we actually passed one in, which isn't a given + name_to_add = scientific_name + elsif parent && parent.default_scientific_name + name_to_add = parent.default_scientific_name + else + logger.warn("Warning: no scientific name (not even on parent crop) for #{self}") end + + if name_to_add + if scientific_names.exists?(:scientific_name => name_to_add) + logger.warn("Warning: skipping duplicate scientific name #{name_to_add} for #{self}") + else + cropbot = Member.find_by_login_name('cropbot') + raise "cropbot account not found: run rake db:seed" unless cropbot + + scientific_names.create( + :scientific_name => name_to_add, + :creator_id => cropbot.id + ) + end + end + end # Crop.search(string) diff --git a/db/seeds.rb b/db/seeds.rb index 5dd1245b6..2aa4fd12b 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -29,7 +29,7 @@ def load_crops Dir.glob("#{source_path}/crops*.csv").each do |crop_file| puts "Loading crops from #{crop_file}..." CSV.foreach(crop_file) do |row| - Crop.create_from_csv(row, definitely_new=true) + Crop.create_from_csv(row) end end puts "Finished loading crops" diff --git a/spec/factories/member.rb b/spec/factories/member.rb index bb4523a33..b8b23a58a 100644 --- a/spec/factories/member.rb +++ b/spec/factories/member.rb @@ -11,6 +11,11 @@ FactoryGirl.define do show_email false bio 'I love seeds' + # cropbot is needed for certain tests, eg. Crop.create_from_csv + factory :cropbot do + login_name 'cropbot' + end + factory :no_tos_member do tos_agreement false end diff --git a/spec/models/crop_spec.rb b/spec/models/crop_spec.rb index 1741faf74..30cb34802 100644 --- a/spec/models/crop_spec.rb +++ b/spec/models/crop_spec.rb @@ -343,4 +343,114 @@ describe Crop do end end + context "csv loading" do + + before(:each) do + # don't use 'let' for this -- we need to actually create it, + # regardless of whether it's used. + @cropbot = FactoryGirl.create(:cropbot) + end + + it "adds a scientific name to a crop that has none" do + tomato = FactoryGirl.create(:tomato) + expect(tomato.scientific_names.size).to eq 0 + tomato.add_scientific_name_from_csv("Foo bar") + expect(tomato.scientific_names.size).to eq 1 + expect(tomato.default_scientific_name).to eq "Foo bar" + end + + it "picks up scientific name from parent crop if available" do + parent = FactoryGirl.create(:crop, :name => 'parent crop') + parent.add_scientific_name_from_csv("Parentis cropis") + parent.save + parent.reload + + tomato = FactoryGirl.create(:tomato, :parent => parent) + expect(tomato.parent).to eq parent + expect(tomato.parent.default_scientific_name).to eq "Parentis cropis" + + tomato.add_scientific_name_from_csv('') + expect(tomato.default_scientific_name).to eq "Parentis cropis" + end + + it "doesn't add a duplicate scientific name" do + tomato = FactoryGirl.create(:tomato) + expect(tomato.scientific_names.size).to eq 0 + tomato.add_scientific_name_from_csv("Foo bar") + expect(tomato.scientific_names.size).to eq 1 + tomato.add_scientific_name_from_csv("Foo bar") + expect(tomato.scientific_names.size).to eq 1 # shouldn't increase + tomato.add_scientific_name_from_csv("Baz quux") + expect(tomato.scientific_names.size).to eq 2 + end + + it "doesn't add a duplicate scientific name from parent" do + parent = FactoryGirl.create(:crop, :name => 'parent') + parent.add_scientific_name_from_csv("Parentis cropis") + parent.save + parent.reload + + tomato = FactoryGirl.create(:tomato, :parent => parent) + expect(tomato.scientific_names.size).to eq 0 + tomato.add_scientific_name_from_csv('') + expect(tomato.scientific_names.size).to eq 1 # picks up parent SN + tomato.add_scientific_name_from_csv('') + expect(tomato.scientific_names.size).to eq 1 # shouldn't increase now + end + + + it "loads the simplest possible crop" do + tomato_row = "tomato,,http://en.wikipedia.org/wiki/Tomato" + + CSV.parse(tomato_row) do |row| + Crop.create_from_csv(row) + end + + loaded = Crop.last + expect(loaded.name).to eq "tomato" + expect(loaded.en_wikipedia_url).to eq 'http://en.wikipedia.org/wiki/Tomato' + expect(loaded.creator).to eq @cropbot + end + + it "loads a crop with a scientific name" do + tomato_row = "tomato,Solanum lycopersicum,http://en.wikipedia.org/wiki/Tomato" + + CSV.parse(tomato_row) do |row| + Crop.create_from_csv(row) + end + + loaded = Crop.last + expect(loaded.name).to eq "tomato" + expect(loaded.scientific_names.size).to eq 1 + expect(loaded.scientific_names.last.scientific_name).to eq "Solanum lycopersicum" + end + + it "loads a crop with a parent" do + parent = FactoryGirl.create(:crop, :name => 'parent') + tomato_row = "tomato,,http://en.wikipedia.org/wiki/Tomato,parent" + + CSV.parse(tomato_row) do |row| + Crop.create_from_csv(row) + end + + loaded = Crop.last + expect(loaded.parent).to eq parent + end + + it "doesn't add unnecessary duplicate crops" do + tomato_row = "tomato,Solanum lycopersicum,http://en.wikipedia.org/wiki/Tomato" + + CSV.parse(tomato_row) do |row| + Crop.create_from_csv(row) + end + + loaded = Crop.last + expect(loaded.name).to eq "tomato" + expect(loaded.en_wikipedia_url).to eq 'http://en.wikipedia.org/wiki/Tomato' + expect(loaded.creator).to eq @cropbot + + end + + end + end From 1d81064ced34b2f11eda9dfd51bbd4e9474ac111 Mon Sep 17 00:00:00 2001 From: Skud Date: Wed, 15 Oct 2014 14:03:00 +0100 Subject: [PATCH 064/132] removed unused Crop.random method --- app/models/crop.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/models/crop.rb b/app/models/crop.rb index e4225f7b4..0188c7955 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -30,10 +30,10 @@ class Crop < ActiveRecord::Base :message => 'is not a valid English Wikipedia URL' } - def Crop.random - @crop = Crop.offset(rand(Crop.count)).first - return @crop - end +# def Crop.random +# @crop = Crop.offset(rand(Crop.count)).first +# return @crop +#aend def to_s return name From 8e6a57c4429eac88ab934f422ab11bf16b0a7663 Mon Sep 17 00:00:00 2001 From: Skud Date: Wed, 15 Oct 2014 15:52:13 +0100 Subject: [PATCH 065/132] Attempt at writing tests for planting reminder email (Test are broken) OK, so I decided to use the capybara_email gem to write these tests. It's basically working but there's a problem with the has_link matcher. Basically the emails sent by ActionMailer have URLs like http://localhost:8080... whereas the tests asking for planting_url() or similar are looking for http://example.com... I don't understand AT ALL why there is this discrepancy, but it looks like ActionMailer is using the settings from the development environment, instead of the testing environment. WHYYYYYY??? I'm pushing this up so that maybe someone else can figure it out, because I'm stumped. --- Gemfile | 1 + Gemfile.lock | 4 ++ spec/features/planting_reminder_spec.rb | 57 +++++++++++++++++++++++++ spec/spec_helper.rb | 2 + 4 files changed, 64 insertions(+) create mode 100644 spec/features/planting_reminder_spec.rb diff --git a/Gemfile b/Gemfile index e47403546..8428c63f3 100644 --- a/Gemfile +++ b/Gemfile @@ -126,5 +126,6 @@ group :development, :test do gem 'factory_girl_rails', '~> 4.0' # for creating test data gem 'coveralls', require: false # coverage analysis gem 'capybara' # integration tests + gem 'capybara-email' # integration tests for email gem 'poltergeist', '~> 1.5.1' # for headless JS testing end diff --git a/Gemfile.lock b/Gemfile.lock index 7829a34de..9127443fb 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -67,6 +67,9 @@ GEM rack (>= 1.0.0) rack-test (>= 0.5.4) xpath (~> 2.0) + capybara-email (2.4.0) + capybara (~> 2.4) + mail chunky_png (1.3.1) cliver (0.3.2) coderay (1.1.0) @@ -308,6 +311,7 @@ DEPENDENCIES bundler (>= 1.1.5) cancan capybara + capybara-email coffee-rails (~> 3.2.1) compass-rails (~> 1.0.3) coveralls diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb new file mode 100644 index 000000000..849b971e5 --- /dev/null +++ b/spec/features/planting_reminder_spec.rb @@ -0,0 +1,57 @@ +require 'spec_helper' +require 'capybara/email/rspec' + +feature "Planting reminder email" do + let(:member) { FactoryGirl.create(:member) } + let(:mail) { Notifier.planting_reminder(member) } + + scenario "has a greeting" do + expect(mail).to have_content "Hello" + end + + context "when member has no plantings" do + let(:member) { FactoryGirl.create(:member) } + let(:mail) { Notifier.planting_reminder(member) } + + scenario "tells you to tracking plantings" do + expect(mail).to have_content "planting your first crop" + end + + scenario "doesn't list plantings" do + expect(mail).not_to have_content "most recent plantings you've told us about" + end + + end + + context "when member has some plantings" do + let(:member) { FactoryGirl.create(:member) } + let(:mail) { Notifier.planting_reminder(member) } + + before :each do + @p1 = FactoryGirl.create(:planting, + :garden => member.gardens.first, + :owner => member + ) + @p2 = FactoryGirl.create(:planting, + :garden => member.gardens.first, + :owner => member + ) + end + + scenario "lists plantings" do + puts Rails.env + puts planting_url(@p1) + expect(mail).to have_content "most recent plantings you've told us about" + expect(mail).to have_link @p1.to_s, :href => planting_url(@p1) + expect(mail).to have_link @p2.to_s, :href => planting_url(@p2) + expect(mail).to have_content "keep your garden records up to date" + end + end + + context "when member has no harvests" do + end + + context "when member has some harvests" do + end + +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 9af45a394..f77c8b267 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -25,6 +25,8 @@ end require 'capybara' require 'capybara/poltergeist' Capybara.javascript_driver = :poltergeist +Capybara.app_host = 'http://localhost' +Capybara.server_port = 8080 include Warden::Test::Helpers From a3e02a3e61bc409b7773cdabe37bead680f66f6f Mon Sep 17 00:00:00 2001 From: Skud Date: Wed, 15 Oct 2014 16:47:43 +0100 Subject: [PATCH 066/132] Actually delete Crop.random, don't just comment it out --- app/models/crop.rb | 5 ----- 1 file changed, 5 deletions(-) diff --git a/app/models/crop.rb b/app/models/crop.rb index 3ae678a9e..f1d5dcc1d 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -33,11 +33,6 @@ class Crop < ActiveRecord::Base :message => 'is not a valid English Wikipedia URL' } -# def Crop.random -# @crop = Crop.offset(rand(Crop.count)).first -# return @crop -#aend - def to_s return name end From 6a7c935d43e8de4e818ff9435f4adb420cd1aa79 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 16 Oct 2014 06:37:02 +1100 Subject: [PATCH 067/132] try setting locale from query parameter first --- app/controllers/application_controller.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb index 1b27f01fc..e417984fa 100644 --- a/app/controllers/application_controller.rb +++ b/app/controllers/application_controller.rb @@ -35,10 +35,10 @@ class ApplicationController < ActionController::Base end def set_locale - I18n.locale = extract_locale_tld || I18n.default_locale + I18n.locale = params[:locale] || extract_locale_from_subdomain || I18n.default_locale end - def extract_locale_tld + def extract_locale_from_subdomain parsed_locale = request.subdomains.first I18n.available_locales.map(&:to_s).include?(parsed_locale) ? parsed_locale : nil end From 0f75a969447e087b0c8607cd18854551bc0264f0 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 16 Oct 2014 06:37:50 +1100 Subject: [PATCH 068/132] write spec to test that setting a different locale fundamentally works --- spec/features/locale_spec.rb | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 spec/features/locale_spec.rb diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb new file mode 100644 index 000000000..bffedfb75 --- /dev/null +++ b/spec/features/locale_spec.rb @@ -0,0 +1,12 @@ +require 'spec_helper' + +feature "Changing locales" do + + scenario "Locale can be set with a query param" do + visit "/" + expect(page).to have_content("Growstuff (dev) is a community of food gardeners.") + visit "/?locale=ja" + expect(page).to have_content("Growstuff (dev)はガーデナーのコミュニティです。") + end + +end \ No newline at end of file From d627d8394e2c3584a366016d30e0084beec4e117 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 16 Oct 2014 06:56:39 +1100 Subject: [PATCH 069/132] forgot which env I was in --- spec/features/locale_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb index bffedfb75..8e7aad90c 100644 --- a/spec/features/locale_spec.rb +++ b/spec/features/locale_spec.rb @@ -4,9 +4,9 @@ feature "Changing locales" do scenario "Locale can be set with a query param" do visit "/" - expect(page).to have_content("Growstuff (dev) is a community of food gardeners.") + expect(page).to have_content("Growstuff (test) is a community of food gardeners.") visit "/?locale=ja" - expect(page).to have_content("Growstuff (dev)はガーデナーのコミュニティです。") + expect(page).to have_content("Growstuff (test)はガーデナーのコミュニティです。") end end \ No newline at end of file From 8994bcf28cf98728f190de6eb9f2bc46a39bf75b Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 16 Oct 2014 07:14:03 +1100 Subject: [PATCH 070/132] ok, we'll just remove all references to environment --- spec/features/locale_spec.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb index 8e7aad90c..37f1b9905 100644 --- a/spec/features/locale_spec.rb +++ b/spec/features/locale_spec.rb @@ -4,9 +4,9 @@ feature "Changing locales" do scenario "Locale can be set with a query param" do visit "/" - expect(page).to have_content("Growstuff (test) is a community of food gardeners.") + expect(page).to have_content("a community of food gardeners.") visit "/?locale=ja" - expect(page).to have_content("Growstuff (test)はガーデナーのコミュニティです。") + expect(page).to have_content("はガーデナーのコミュニティです。") end end \ No newline at end of file From 2515d34277a3420d71d25ab0553cc53b3e3c752b Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 16 Oct 2014 08:09:20 +1100 Subject: [PATCH 071/132] add teardown to reset locale to default en --- spec/features/locale_spec.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb index 37f1b9905..9a787f6cc 100644 --- a/spec/features/locale_spec.rb +++ b/spec/features/locale_spec.rb @@ -2,6 +2,10 @@ require 'spec_helper' feature "Changing locales" do + after do + I18n.locale = :en + end + scenario "Locale can be set with a query param" do visit "/" expect(page).to have_content("a community of food gardeners.") From e85cb4598db71803b76ca093f216c28181f0f603 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 12:38:44 +0100 Subject: [PATCH 072/132] Add alternate name model. --- app/models/alternate_name.rb | 5 +++++ .../20141018111015_create_alternate_names.rb | 11 +++++++++++ db/schema.rb | 11 ++++++++++- spec/factories/alternate_names.rb | 14 ++++++++++++++ spec/models/alternate_name_spec.rb | 11 +++++++++++ 5 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 app/models/alternate_name.rb create mode 100644 db/migrate/20141018111015_create_alternate_names.rb create mode 100644 spec/factories/alternate_names.rb create mode 100644 spec/models/alternate_name_spec.rb diff --git a/app/models/alternate_name.rb b/app/models/alternate_name.rb new file mode 100644 index 000000000..fb92a2bc9 --- /dev/null +++ b/app/models/alternate_name.rb @@ -0,0 +1,5 @@ +class AlternateName < ActiveRecord::Base + attr_accessible :crop_id, :name, :creator_id + belongs_to :crop + belongs_to :creator, :class_name => 'Member' +end diff --git a/db/migrate/20141018111015_create_alternate_names.rb b/db/migrate/20141018111015_create_alternate_names.rb new file mode 100644 index 000000000..8a63471c8 --- /dev/null +++ b/db/migrate/20141018111015_create_alternate_names.rb @@ -0,0 +1,11 @@ +class CreateAlternateNames < ActiveRecord::Migration + def change + create_table :alternate_names do |t| + t.string :name, :null => false + t.integer :crop_id, :null => false + t.integer :creator_id, :null => false + + t.timestamps + end + end +end diff --git a/db/schema.rb b/db/schema.rb index 9292a6fbb..39a68cab2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -11,7 +11,7 @@ # # It's strongly recommended to check this file into your version control system. -ActiveRecord::Schema.define(:version => 20141002022459) do +ActiveRecord::Schema.define(:version => 20141018111015) do create_table "account_types", :force => true do |t| t.string "name", :null => false @@ -29,6 +29,14 @@ ActiveRecord::Schema.define(:version => 20141002022459) do t.datetime "updated_at", :null => false end + create_table "alternate_names", :force => true do |t| + t.string "name", :null => false + t.integer "crop_id", :null => false + t.integer "creator_id", :null => false + t.datetime "created_at", :null => false + t.datetime "updated_at", :null => false + end + create_table "authentications", :force => true do |t| t.integer "member_id", :null => false t.string "provider", :null => false @@ -154,6 +162,7 @@ ActiveRecord::Schema.define(:version => 20141002022459) do t.text "bio" t.integer "plantings_count" t.boolean "newsletter" + t.boolean "send_planting_reminder", :default => true end add_index "members", ["confirmation_token"], :name => "index_users_on_confirmation_token", :unique => true diff --git a/spec/factories/alternate_names.rb b/spec/factories/alternate_names.rb new file mode 100644 index 000000000..72c73d99c --- /dev/null +++ b/spec/factories/alternate_names.rb @@ -0,0 +1,14 @@ +# Read about factories at https://github.com/thoughtbot/factory_girl + +FactoryGirl.define do + factory :alternate_name do + association :crop, factory: :crop + name "alternate name" + creator + + factory :alternate_tomato do + association :crop, factory: :tomato + name "alternative tomato" + end + end +end diff --git a/spec/models/alternate_name_spec.rb b/spec/models/alternate_name_spec.rb new file mode 100644 index 000000000..f91d6dc1f --- /dev/null +++ b/spec/models/alternate_name_spec.rb @@ -0,0 +1,11 @@ +require 'spec_helper' + +describe AlternateName do + before (:each) do + @an = FactoryGirl.create(:alternate_tomato) + end + + it 'should save a basic alternate name' do + @an.save.should be_true + end +end From 83fa291060dc005b9965c1717c2c6c4bb1a628c6 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 13:49:57 +0100 Subject: [PATCH 073/132] Fetch alternate names for a crop. --- app/models/crop.rb | 1 + spec/models/alternate_name_spec.rb | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/app/models/crop.rb b/app/models/crop.rb index 387936d91..fa99ed90c 100644 --- a/app/models/crop.rb +++ b/app/models/crop.rb @@ -8,6 +8,7 @@ class Crop < ActiveRecord::Base :allow_destroy => true, :reject_if => :all_blank + has_many :alternate_names has_many :plantings has_many :photos, :through => :plantings has_many :seeds diff --git a/spec/models/alternate_name_spec.rb b/spec/models/alternate_name_spec.rb index f91d6dc1f..e7772dca0 100644 --- a/spec/models/alternate_name_spec.rb +++ b/spec/models/alternate_name_spec.rb @@ -8,4 +8,17 @@ describe AlternateName do it 'should save a basic alternate name' do @an.save.should be_true end + + it 'should be possible to add multiple alternate names to a crop' do + crop = @an.crop + an2 = AlternateName.create( + :name => "really alternative tomato", + :crop_id => crop.id, + :creator_id => @an.creator.id + ) + crop.alternate_names << an2 + crop.alternate_names.should include @an + crop.alternate_names.should include an2 + end + end From 9ededef54d6b770b7c308035c6946d53ee4c9590 Mon Sep 17 00:00:00 2001 From: Skud Date: Sat, 18 Oct 2014 14:00:22 +0100 Subject: [PATCH 074/132] Added tests for content of planting reminder email --- Gemfile.lock | 41 +++++++++-------- .../harvests/harvesting_a_crop_spec.rb | 3 +- spec/features/planting_reminder_spec.rb | 44 ++++++++++++++----- .../plantings/planting_a_crop_spec.rb | 2 +- 4 files changed, 55 insertions(+), 35 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 53b817bdb..32d80d3c3 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -61,7 +61,7 @@ GEM railties (>= 3.0) builder (3.0.4) cancan (1.6.10) - capybara (2.4.1) + capybara (2.4.4) mime-types (>= 1.16) nokogiri (>= 1.3.3) rack (>= 1.0.0) @@ -70,7 +70,7 @@ GEM capybara-email (2.4.0) capybara (~> 2.4) mail - chunky_png (1.3.1) + chunky_png (1.3.2) cliver (0.3.2) coderay (1.1.0) coffee-rails (3.2.2) @@ -118,15 +118,14 @@ GEM thread thread_safe erubis (2.7.0) - execjs (2.2.1) - factory_girl (4.4.0) + execjs (2.2.2) + factory_girl (4.5.0) activesupport (>= 3.0.0) - factory_girl_rails (4.4.1) - factory_girl (~> 4.4.0) + factory_girl_rails (4.5.0) + factory_girl (~> 4.5.0) railties (>= 3.0.0) - figaro (0.7.0) - bundler (~> 1.0) - rails (>= 3, < 5) + figaro (1.0.0) + thor (~> 0.14) flickraw (0.9.8) friendly_id (4.0.10.1) activerecord (>= 3.0, < 4.0) @@ -151,7 +150,7 @@ GEM multi_json (~> 1.0) multi_xml (>= 0.5.2) i18n (0.6.1) - i18n-tasks (0.7.6) + i18n-tasks (0.7.7) activesupport easy_translate (>= 0.5.0) erubis @@ -161,7 +160,7 @@ GEM term-ansicolor terminal-table journey (1.0.4) - jquery-rails (3.1.1) + jquery-rails (3.1.2) railties (>= 3.0, < 5.0) thor (>= 0.14, < 2.0) jquery-ui-rails (4.1.2) @@ -175,7 +174,7 @@ GEM addressable (~> 2.3) leaflet-markercluster-rails (0.7.0) railties (>= 3.1) - leaflet-rails (0.7.3) + leaflet-rails (0.7.4) less (2.5.1) commonjs (~> 0.2.7) less-rails (2.5.0) @@ -195,8 +194,8 @@ GEM mini_portile (0.6.0) multi_json (1.10.1) multi_xml (0.5.5) - netrc (0.7.7) - newrelic_rpm (3.9.3.241) + netrc (0.8.0) + newrelic_rpm (3.9.5.251) nokogiri (1.6.3.1) mini_portile (= 0.6.0) oauth (0.4.7) @@ -208,7 +207,7 @@ GEM omniauth-oauth (1.0.1) oauth omniauth (~> 1.0) - omniauth-twitter (1.0.1) + omniauth-twitter (1.1.0) multi_json (~> 1.3) omniauth-oauth (~> 1.0) orm_adapter (0.5.0) @@ -219,7 +218,7 @@ GEM multi_json (~> 1.0) websocket-driver (>= 0.2.0) polyglot (0.3.5) - pry (0.10.0) + pry (0.10.1) coderay (~> 1.1.0) method_source (~> 0.8.1) slop (~> 3.4) @@ -238,7 +237,7 @@ GEM activesupport (= 3.2.13) bundler (~> 1.0) railties (= 3.2.13) - rails_12factor (0.0.2) + rails_12factor (0.0.3) rails_serve_static_assets rails_stdout_logging rails_serve_static_assets (0.0.2) @@ -274,9 +273,9 @@ GEM railties (~> 3.2.0) sass (>= 3.1.10) tilt (~> 1.3) - simplecov (0.9.0) + simplecov (0.9.1) docile (~> 1.1.0) - multi_json + multi_json (~> 1.0) simplecov-html (~> 0.8.0) simplecov-html (0.8.0) slop (3.6.0) @@ -297,7 +296,7 @@ GEM thread (0.1.4) thread_safe (0.3.4) tilt (1.4.1) - tins (1.3.2) + tins (1.3.3) treetop (1.4.15) polyglot polyglot (>= 0.3.1) @@ -315,7 +314,7 @@ GEM nokogiri (>= 1.2.0) rack (>= 1.0) rack-test (>= 0.5.3) - websocket-driver (0.3.4) + websocket-driver (0.3.5) will_paginate (3.0.7) xpath (2.0.0) nokogiri (~> 1.3) diff --git a/spec/features/harvests/harvesting_a_crop_spec.rb b/spec/features/harvests/harvesting_a_crop_spec.rb index 7a366309a..0feb29618 100644 --- a/spec/features/harvests/harvesting_a_crop_spec.rb +++ b/spec/features/harvests/harvesting_a_crop_spec.rb @@ -55,4 +55,5 @@ feature "Harvesting a crop", :js => true do end -end \ No newline at end of file +end + diff --git a/spec/features/planting_reminder_spec.rb b/spec/features/planting_reminder_spec.rb index 849b971e5..5adb44095 100644 --- a/spec/features/planting_reminder_spec.rb +++ b/spec/features/planting_reminder_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' require 'capybara/email/rspec' -feature "Planting reminder email" do +feature "Planting reminder email", :js => true do let(:member) { FactoryGirl.create(:member) } let(:mail) { Notifier.planting_reminder(member) } @@ -10,10 +10,7 @@ feature "Planting reminder email" do end context "when member has no plantings" do - let(:member) { FactoryGirl.create(:member) } - let(:mail) { Notifier.planting_reminder(member) } - - scenario "tells you to tracking plantings" do + scenario "tells you to track your plantings" do expect(mail).to have_content "planting your first crop" end @@ -24,9 +21,6 @@ feature "Planting reminder email" do end context "when member has some plantings" do - let(:member) { FactoryGirl.create(:member) } - let(:mail) { Notifier.planting_reminder(member) } - before :each do @p1 = FactoryGirl.create(:planting, :garden => member.gardens.first, @@ -39,19 +33,45 @@ feature "Planting reminder email" do end scenario "lists plantings" do - puts Rails.env - puts planting_url(@p1) expect(mail).to have_content "most recent plantings you've told us about" - expect(mail).to have_link @p1.to_s, :href => planting_url(@p1) - expect(mail).to have_link @p2.to_s, :href => planting_url(@p2) + expect(mail).to have_content @p1.to_s + expect(mail).to have_content @p2.to_s + # can't test for links to your plantings due to this weirdness: + # https://github.com/Skud/growstuff/commit/8e6a57c4429eac88ab934f422ab11bf16b0a7663 expect(mail).to have_content "keep your garden records up to date" end end context "when member has no harvests" do + scenario "tells you to tracking plantings" do + expect(mail).to have_content "Get started now by tracking your first harvest" + end + + scenario "doesn't list plantings" do + expect(mail).not_to have_content "the last few things you harvested were" + end + end context "when member has some harvests" do + before :each do + @h1 = FactoryGirl.create(:harvest, + :owner => member + ) + @h2 = FactoryGirl.create(:harvest, + :owner => member + ) + end + + scenario "lists harvests" do + expect(mail).to have_content "the last few things you harvested were" + expect(mail).to have_content @h1.to_s + expect(mail).to have_content @h2.to_s + # can't test for links to your harvests due to this weirdness: + # https://github.com/Skud/growstuff/commit/8e6a57c4429eac88ab934f422ab11bf16b0a7663 + expect(mail).to have_content "Harvested anything else lately?" + end + end end diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index d8fae5c64..be5fc1142 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -65,7 +65,7 @@ feature "Planting a crop", :js => true do # The finished at date was cached in Javascript in # case the user clicks unfinished accidentally. expect(page.find("#planting_finished_at").value).to eq("2014-08-30") - + within "form#new_planting" do click_button "Save" end From 627587ee1b5468bf719e978a850b85be4627bd5a Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 14:19:09 +0100 Subject: [PATCH 075/132] Show alternate names on crop page --- app/views/crops/show.html.haml | 6 ++++++ spec/factories/alternate_names.rb | 8 ++++---- spec/factories/crop.rb | 3 +++ spec/features/crop_spec.rb | 10 ++++++++++ 4 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 spec/features/crop_spec.rb diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 670a0d438..b031ec5b2 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -72,6 +72,12 @@ - if can? :edit, @crop = link_to 'Add', new_scientific_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } + %h4 Alternate names + %ul + - @crop.alternate_names.each do |an| + %li + = an.name + = render :partial => 'varieties', :locals => { :crop => @crop } = render :partial => 'grown_for', :locals => { :crop => @crop } diff --git a/spec/factories/alternate_names.rb b/spec/factories/alternate_names.rb index 72c73d99c..1c14d60ac 100644 --- a/spec/factories/alternate_names.rb +++ b/spec/factories/alternate_names.rb @@ -2,13 +2,13 @@ FactoryGirl.define do factory :alternate_name do - association :crop, factory: :crop name "alternate name" + crop creator - factory :alternate_tomato do - association :crop, factory: :tomato - name "alternative tomato" + factory :alternate_eggplant do + association :crop, factory: :eggplant + name "aubergine" end end end diff --git a/spec/factories/crop.rb b/spec/factories/crop.rb index 975e8d323..c6839e983 100644 --- a/spec/factories/crop.rb +++ b/spec/factories/crop.rb @@ -40,6 +40,9 @@ FactoryGirl.define do name "popcorn" end + factory :eggplant do + name "eggplant" + end # This should have a name that is alphabetically earlier than :uppercase # crop to ensure that the ordering tests work. diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb new file mode 100644 index 000000000..9a2bb273b --- /dev/null +++ b/spec/features/crop_spec.rb @@ -0,0 +1,10 @@ +require 'spec_helper' + +feature "Alternate names" do + let(:alternate_eggplant) { FactoryGirl.create(:alternate_eggplant) } + + scenario "Display alternate names on crop page" do + visit crop_path(alternate_eggplant.crop) + expect(page).to have_content alternate_eggplant.name + end +end From ae27a0a5b56a8c481e852346500a239959f79a7a Mon Sep 17 00:00:00 2001 From: Skud Date: Sat, 18 Oct 2014 14:38:00 +0100 Subject: [PATCH 076/132] Added some additional hierarchy around cherry tomatoes --- db/seeds/crops-11-tomatoes.csv | 78 +++++++++++++++++----------------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/db/seeds/crops-11-tomatoes.csv b/db/seeds/crops-11-tomatoes.csv index eb28aaa79..acb39a039 100644 --- a/db/seeds/crops-11-tomatoes.csv +++ b/db/seeds/crops-11-tomatoes.csv @@ -1,41 +1,41 @@ -adoration tomato,,https://en.wikipedia.org/wiki/Adoration_%28Tomato%29,tomato, -alicante tomato,,https://en.wikipedia.org/wiki/Alicante_%28tomato%29,tomato, -Amish paste tomato,,http://en.wikipedia.org/wiki/Amish_Paste,tomato, -azoychka tomato,,https://en.wikipedia.org/wiki/Azoychka%28Tomato%29,tomato, -beefsteak tomato,,https://en.wikipedia.org/wiki/Beefsteak_(tomato),tomato, -better boy tomato,,https://en.wikipedia.org/wiki/Better_Boy,tomato, -big rainbow tomato,,https://en.wikipedia.org/wiki/Big_Rainbow_(Tomato),tomato, -Blaby special tomato,,https://en.wikipedia.org/wiki/Blaby_Special_(Tomato),tomato, -black krim tomato,,https://en.wikipedia.org/wiki/Black_Krim_%28tomato%29,tomato, -brandywine tomato,,https://en.wikipedia.org/wiki/Brandywine_(tomato),tomato, -campari tomato,,https://en.wikipedia.org/wiki/Campari_tomato,tomato, -Cherokee purple tomato,,https://en.wikipedia.org/wiki/Cherokee_purple,tomato, +adoration tomato,,https://en.wikipedia.org/wiki/Adoration_%28Tomato%29,tomato +alicante tomato,,https://en.wikipedia.org/wiki/Alicante_%28tomato%29,tomato +Amish paste tomato,,http://en.wikipedia.org/wiki/Amish_Paste,tomato +Aunt Ruby's German green tomato,,http://en.wikipedia.org/wiki/Aunt_Ruby%27s_German_Green,tomato +azoychka tomato,,https://en.wikipedia.org/wiki/Azoychka%28Tomato%29,tomato +beefsteak tomato,,https://en.wikipedia.org/wiki/Beefsteak_(tomato),tomato +better boy tomato,,https://en.wikipedia.org/wiki/Better_Boy,tomato +big rainbow tomato,,https://en.wikipedia.org/wiki/Big_Rainbow_(Tomato),tomato +Blaby special tomato,,https://en.wikipedia.org/wiki/Blaby_Special_(Tomato),tomato +black krim tomato,,https://en.wikipedia.org/wiki/Black_Krim_%28tomato%29,tomato +brandywine tomato,,https://en.wikipedia.org/wiki/Brandywine_(tomato),tomato +campari tomato,,https://en.wikipedia.org/wiki/Campari_tomato,tomato +celebrity tomato,,http://en.wikipedia.org/wiki/Celebrity_(tomato),tomato +Cherokee purple tomato,,https://en.wikipedia.org/wiki/Cherokee_purple,tomato +cherry tomato,Solanum lycopersicum var. cerasiforme,http://en.wikipedia.org/wiki/Cherry_tomato,tomato currant tomato,solanum pimpinellifolium,https://en.wikipedia.org/wiki/Solanum_pimpinellifolium, -early girl tomato,,https://en.wikipedia.org/wiki/Early_Girl,tomato, -Fourth of July tomato,,https://en.wikipedia.org/wiki/Fourth_of_July_(tomato_variety),tomato, -garden peach tomato,,https://en.wikipedia.org/wiki/Garden_peach_tomato,tomato, -green zebra tomato,,https://en.wikipedia.org/wiki/Green_Zebra,tomato, -hillbilly tomato,,http://en.wikipedia.org/wiki/Hillbilly_(tomato),tomato, -jubilee tomato,,http://en.wikipedia.org/wiki/Jubilee_(tomato),tomato, -lillian's yellow tomato,,http://en.wikipedia.org/wiki/Lillian%27s_Yellow_(tomato),tomato, -Matt's wild cherry tomato,,http://en.wikipedia.org/wiki/Matt%27s_Wild_Cherry,tomato, -mortgage lifter tomato,,http://en.wikipedia.org/wiki/Mortgage_Lifter,tomato, -Mr. Stripey tomato,,http://en.wikipedia.org/wiki/Mr._Stripey,tomato, -Roma tomato,,http://en.wikipedia.org/wiki/Roma_tomato,tomato, -San Marzano tomato,,http://en.wikipedia.org/wiki/San_Marzano_tomato,tomato, -Santorini tomato,,http://en.wikipedia.org/wiki/Santorini_(tomato),tomato, -stupice tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,tomato, -tigerella tomato,,http://en.wikipedia.org/wiki/Tigerella,tomato, -tomaccio tomato,,http://en.wikipedia.org/wiki/Tomaccio_(tomato),tomato, +early girl tomato,,https://en.wikipedia.org/wiki/Early_Girl,tomato +Fourth of July tomato,,https://en.wikipedia.org/wiki/Fourth_of_July_(tomato_variety),tomato +garden peach tomato,,https://en.wikipedia.org/wiki/Garden_peach_tomato,tomato +grape tomato,,http://en.wikipedia.org/wiki/Grape_tomato,tomato +green zebra tomato,,https://en.wikipedia.org/wiki/Green_Zebra,tomato +Hanover tomato,,http://en.wikipedia.org/wiki/Hanover_tomato,tomato +hillbilly tomato,,http://en.wikipedia.org/wiki/Hillbilly_(tomato),tomato +jubilee tomato,,http://en.wikipedia.org/wiki/Jubilee_(tomato),tomato +lillian's yellow tomato,,http://en.wikipedia.org/wiki/Lillian%27s_Yellow_(tomato),tomato +marglobe tomato,,http://en.wikipedia.org/wiki/Marglobe,tomato +Matt's wild cherry tomato,,http://en.wikipedia.org/wiki/Matt%27s_Wild_Cherry,cherry tomato +mortgage lifter tomato,,http://en.wikipedia.org/wiki/Mortgage_Lifter,tomato +Mr. Stripey tomato,,http://en.wikipedia.org/wiki/Mr._Stripey,tomato +pear tomato,,http://en.wikipedia.org/wiki/Pear_tomato,tomato +Roma tomato,,http://en.wikipedia.org/wiki/Roma_tomato,tomato +San Marzano tomato,,http://en.wikipedia.org/wiki/San_Marzano_tomato,tomato +Santorini tomato,,http://en.wikipedia.org/wiki/Santorini_(tomato),cherry tomato +stupice tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,tomato +super sweet 100 tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,cherry tomato +three sisters tomato,,http://en.wikipedia.org/wiki/Three_Sisters_(tomato),tomato +tigerella tomato,,http://en.wikipedia.org/wiki/Tigerella,tomato +tomaccio tomato,,http://en.wikipedia.org/wiki/Tomaccio_(tomato),cherry tomato +tomberry,,http://en.wikipedia.org/wiki/Tomberry,tomato traveller tomato,,http://en.wikipedia.org/wiki/Traveller_(tomato),tomato -three sisters tomato,,http://en.wikipedia.org/wiki/Three_Sisters_(tomato),tomato, -Hanover tomato,,http://en.wikipedia.org/wiki/Hanover_tomato,tomato, -celebrity tomato,,http://en.wikipedia.org/wiki/Celebrity_(tomato),tomato, -tomberry,,http://en.wikipedia.org/wiki/Tomberry,tomato, -super sweet 100 tomato,,http://en.wikipedia.org/wiki/Super_Sweet_100,tomato, -marglobe tomato,,http://en.wikipedia.org/wiki/Marglobe,tomato, -grape tomato,,http://en.wikipedia.org/wiki/Grape_tomato,tomato, -cherry tomato,,http://en.wikipedia.org/wiki/Cherry_tomato,tomato, -Aunt Ruby's German green tomato,,http://en.wikipedia.org/wiki/Aunt_Ruby%27s_German_Green,tomato, -white queen tomato,,http://en.wikipedia.org/wiki/White_Queen_tomato,tomato, -pear tomato,,http://en.wikipedia.org/wiki/Pear_tomato,tomato, +white queen tomato,,http://en.wikipedia.org/wiki/White_Queen_tomato,tomato From 7e4b51d1cb0656c1b3f4293c6732ce9c1a4d0c11 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 15:18:23 +0100 Subject: [PATCH 077/132] Add "Edit" link for alternate names on crops page. --- app/controllers/alternate_names_controller.rb | 8 +++++++ app/models/ability.rb | 1 + app/views/crops/show.html.haml | 2 ++ config/routes.rb | 1 + spec/features/crop_spec.rb | 23 +++++++++++++++++++ 5 files changed, 35 insertions(+) create mode 100644 app/controllers/alternate_names_controller.rb diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb new file mode 100644 index 000000000..23028693c --- /dev/null +++ b/app/controllers/alternate_names_controller.rb @@ -0,0 +1,8 @@ +class AlternateNamesController < ApplicationController + load_and_authorize_resource + + # GET /alternate_names/1/edit + def edit + @alternate_name = AlternateName.find(params[:id]) + end +end diff --git a/app/models/ability.rb b/app/models/ability.rb index e33f4a022..3e76c6738 100644 --- a/app/models/ability.rb +++ b/app/models/ability.rb @@ -40,6 +40,7 @@ class Ability can :wrangle, Crop can :manage, Crop can :manage, ScientificName + can :manage, AlternateName end # can create & destroy their own authentications against other sites. diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index b031ec5b2..c7d02d8c3 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -77,6 +77,8 @@ - @crop.alternate_names.each do |an| %li = an.name + - if can? :edit, an + = link_to 'Edit', edit_alternate_name_path(an), { :class => 'btn btn-default btn-xs' } = render :partial => 'varieties', :locals => { :crop => @crop } diff --git a/config/routes.rb b/config/routes.rb index 0d645a1be..ac6885e54 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -28,6 +28,7 @@ Growstuff::Application.routes.draw do match '/posts/author/:author' => 'posts#index', :as => 'posts_by_author' resources :scientific_names + resources :alternate_names match 'crops/wrangle' => 'crops#wrangle', :as => 'wrangle_crops' match 'crops/hierarchy' => 'crops#hierarchy', :as => 'crops_hierarchy' diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 9a2bb273b..3b61c3ec5 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -7,4 +7,27 @@ feature "Alternate names" do visit crop_path(alternate_eggplant.crop) expect(page).to have_content alternate_eggplant.name end + + context "User is a crop wrangler" do + let!(:crop_wranglers) { FactoryGirl.create_list(:crop_wrangling_member, 3) } + let(:member){crop_wranglers.first} + + before :each do + visit root_path + click_link 'Sign in' + fill_in 'Login', with: member.login_name + fill_in 'Password', with: member.password + click_button 'Sign in' + page.should have_content member.login_name + end + + scenario "Crop wranglers can edit alternate names" do + visit crop_path(alternate_eggplant.crop) + expect(page).to have_content "CROP WRANGLER" + expect(page).to have_content alternate_eggplant.name + expect(page).to have_link "Edit", :href => edit_alternate_name_path(alternate_eggplant) + end + + end + end From cdf6d4b09ae6e289441076146c6516ced79b3b51 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 15:29:21 +0100 Subject: [PATCH 078/132] Delete alternate name button on crops page. --- app/views/crops/show.html.haml | 2 ++ spec/features/crop_spec.rb | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index c7d02d8c3..5d74f6d8a 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -79,6 +79,8 @@ = an.name - if can? :edit, an = link_to 'Edit', edit_alternate_name_path(an), { :class => 'btn btn-default btn-xs' } + - if can? :destroy, an + = link_to 'Delete', an, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' = render :partial => 'varieties', :locals => { :crop => @crop } diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 3b61c3ec5..9f825015f 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -28,6 +28,12 @@ feature "Alternate names" do expect(page).to have_link "Edit", :href => edit_alternate_name_path(alternate_eggplant) end + scenario "Crop wranglers can delete alternate names" do + visit crop_path(alternate_eggplant.crop) + expect(page).to have_link "Delete", + href: alternate_name_path(alternate_eggplant) + end + end end From 77d1d067fa0fa6d74accda5f54795633574ff579 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 15:35:57 +0100 Subject: [PATCH 079/132] Crop wranglers can add alternate names --- app/views/crops/show.html.haml | 3 +++ spec/features/crop_spec.rb | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 5d74f6d8a..ad5abab1b 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -81,6 +81,9 @@ = link_to 'Edit', edit_alternate_name_path(an), { :class => 'btn btn-default btn-xs' } - if can? :destroy, an = link_to 'Delete', an, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' + %p + - if can? :edit, @crop + = link_to 'Add', new_alternate_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } = render :partial => 'varieties', :locals => { :crop => @crop } diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 9f825015f..52620da81 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -34,6 +34,12 @@ feature "Alternate names" do href: alternate_name_path(alternate_eggplant) end + scenario "Crop wranglers can add alternate names" do + crop = alternate_eggplant.crop + visit crop_path(crop) + expect(page).to have_link "Add", + href: new_alternate_name_path(crop_id: crop.id) + end end end From 29a5fe07c2e2d4d0efb284e000703e44f741fde0 Mon Sep 17 00:00:00 2001 From: Skud Date: Sat, 18 Oct 2014 16:01:00 +0100 Subject: [PATCH 080/132] use rails paths when visiting pages --- spec/features/harvests/harvesting_a_crop_spec.rb | 4 ++-- spec/features/locale_spec.rb | 8 ++++---- spec/features/plantings/planting_a_crop_spec.rb | 4 ++-- spec/features/seeds/adding_seeds_spec.rb | 4 ++-- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/spec/features/harvests/harvesting_a_crop_spec.rb b/spec/features/harvests/harvesting_a_crop_spec.rb index 0feb29618..caffb237d 100644 --- a/spec/features/harvests/harvesting_a_crop_spec.rb +++ b/spec/features/harvests/harvesting_a_crop_spec.rb @@ -6,7 +6,7 @@ feature "Harvesting a crop", :js => true do background do login_as(member) - visit '/harvests/new' + visit new_harvest_path end it_behaves_like "crop suggest", "harvest", "crop" @@ -26,7 +26,7 @@ feature "Harvesting a crop", :js => true do end scenario "Harvesting from crop page" do - visit "/crops/maize" + visit crop_path(maize) click_link "Harvest this" within "form#new_harvest" do expect(page).to have_selector "input[value='maize']" diff --git a/spec/features/locale_spec.rb b/spec/features/locale_spec.rb index 9a787f6cc..b5893858d 100644 --- a/spec/features/locale_spec.rb +++ b/spec/features/locale_spec.rb @@ -7,10 +7,10 @@ feature "Changing locales" do end scenario "Locale can be set with a query param" do - visit "/" + visit root_path expect(page).to have_content("a community of food gardeners.") - visit "/?locale=ja" - expect(page).to have_content("はガーデナーのコミュニティです。") + visit root_path(:locale => 'ja') + expect(page).to have_content("はガーデナーのコミュニティです。") end -end \ No newline at end of file +end diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index be5fc1142..ab120fa48 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -8,7 +8,7 @@ feature "Planting a crop", :js => true do background do login_as(member) - visit "/plantings/new" + visit new_planting_path end it_behaves_like "crop suggest", "planting", "crop" @@ -29,7 +29,7 @@ feature "Planting a crop", :js => true do end scenario "Planting from crop page" do - visit "/crops/maize" + visit crop_path(maize) click_link "Plant this" within "form#new_planting" do expect(page).to have_selector "input[value='maize']" diff --git a/spec/features/seeds/adding_seeds_spec.rb b/spec/features/seeds/adding_seeds_spec.rb index 7df57b95a..ca79c5b8c 100644 --- a/spec/features/seeds/adding_seeds_spec.rb +++ b/spec/features/seeds/adding_seeds_spec.rb @@ -6,7 +6,7 @@ feature "Harvesting a crop", :js => true do background do login_as(member) - visit '/seeds/new' + visit new_seed_path end it_behaves_like "crop suggest", "seed", "crop" @@ -26,7 +26,7 @@ feature "Harvesting a crop", :js => true do end scenario "Adding a seed from crop page" do - visit "/crops/maize" + visit crop_path(maize) click_link "Add seeds to stash" within "form#new_seed" do expect(page).to have_selector "input[value='maize']" From 10f6214c6ced4a8a1a1bfdc421486f1bb2d689e9 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 16:34:38 +0100 Subject: [PATCH 081/132] Views for adding/editing alternate names. --- app/controllers/alternate_names_controller.rb | 12 +++++++++ app/views/alternate_names/_form.html.haml | 25 +++++++++++++++++++ app/views/alternate_names/edit.html.haml | 9 +++++++ app/views/alternate_names/new.html.haml | 3 +++ spec/features/crop_spec.rb | 13 ++++++++++ 5 files changed, 62 insertions(+) create mode 100644 app/views/alternate_names/_form.html.haml create mode 100644 app/views/alternate_names/edit.html.haml create mode 100644 app/views/alternate_names/new.html.haml diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index 23028693c..b28e51f34 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -5,4 +5,16 @@ class AlternateNamesController < ApplicationController def edit @alternate_name = AlternateName.find(params[:id]) end + + # GET /alternate_names/new + # GET /alternate_names/new.json + def new + @alternate_name = AlternateName.new + @crop = Crop.find_by_id(params[:crop_id]) || Crop.new + + respond_to do |format| + format.html # new.html.haml + format.json { render json: @alternate_name } + end + end end diff --git a/app/views/alternate_names/_form.html.haml b/app/views/alternate_names/_form.html.haml new file mode 100644 index 000000000..7d6f9abd5 --- /dev/null +++ b/app/views/alternate_names/_form.html.haml @@ -0,0 +1,25 @@ += form_for @alternate_name, :html => {:class => 'form-horizontal', :role => "form"} do |f| + - if @alternate_name.errors.any? + #error_explanation + %h2= "#{pluralize(@alternate_name.errors.count, "error")} prohibited this alternate_name from being saved:" + %ul + - @alternate_name.errors.full_messages.each do |msg| + %li= msg + + %p + %span.help-block + For detailed crop wrangling guidelines, please consult the + =link_to "crop wrangling guide", "http://wiki.growstuff.org/index.php/Crop_wrangling" + on the Growstuff wiki. + + .form-group + = f.label :crop_id, :class => 'control-label col-md-2' + .col-md-8 + = collection_select(:alternate_name, :crop_id, Crop.all, :id, :name, { :selected => @alternate_name.crop_id || @crop.id }, :class => 'form-control') + .form-group + = f.label :name, :class => 'control-label col-md-2' + .col-md-8 + = f.text_field :name, :class => 'form-control' + .form-group + .form-actions.col-md-offset-2.col-md-8 + = f.submit 'Save', :class => 'btn btn-primary' diff --git a/app/views/alternate_names/edit.html.haml b/app/views/alternate_names/edit.html.haml new file mode 100644 index 000000000..0818bb0ff --- /dev/null +++ b/app/views/alternate_names/edit.html.haml @@ -0,0 +1,9 @@ +- content_for :title, "Edit alternate name" + +%p + Added by + = @alternate_name.creator + = distance_of_time_in_words(@alternate_name.created_at, Time.zone.now) + ago. + += render 'form' diff --git a/app/views/alternate_names/new.html.haml b/app/views/alternate_names/new.html.haml new file mode 100644 index 000000000..7c51473b7 --- /dev/null +++ b/app/views/alternate_names/new.html.haml @@ -0,0 +1,3 @@ +- content_for :title, "New alternate name" + += render 'form' diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 52620da81..ecaa6b224 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -40,6 +40,19 @@ feature "Alternate names" do expect(page).to have_link "Add", href: new_alternate_name_path(crop_id: crop.id) end + + scenario "The add-alternate-name page works" do + crop = alternate_eggplant.crop + visit new_alternate_name_path(crop_id: crop.id) + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + end + + scenario "The edit-alternate-name page works" do + crop = alternate_eggplant.crop + visit edit_alternate_name_path(alternate_eggplant) + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + end + end end From 37eb2a0e34e105de0a49826ad07e0c4a19723d17 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 16:49:27 +0100 Subject: [PATCH 082/132] Show alternate names on their own page. --- app/controllers/alternate_names_controller.rb | 11 +++++++++++ app/views/alternate_names/show.html.haml | 13 +++++++++++++ spec/features/crop_spec.rb | 5 +++++ 3 files changed, 29 insertions(+) create mode 100644 app/views/alternate_names/show.html.haml diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index b28e51f34..af53f9350 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -6,6 +6,17 @@ class AlternateNamesController < ApplicationController @alternate_name = AlternateName.find(params[:id]) end + # GET /alternate_names/1 + # GET /alternate_names/1.json + def show + @alternate_name = AlternateName.find(params[:id]) + + respond_to do |format| + format.html # show.html.haml + format.json { render json: @alternate_name } + end + end + # GET /alternate_names/new # GET /alternate_names/new.json def new diff --git a/app/views/alternate_names/show.html.haml b/app/views/alternate_names/show.html.haml new file mode 100644 index 000000000..2f7db7f14 --- /dev/null +++ b/app/views/alternate_names/show.html.haml @@ -0,0 +1,13 @@ +%p#notice= notice + +%p + %b Alternate name: + = @alternate_name.name +%p + %b Crop: + = link_to @alternate_name.crop + +- if can? :edit, @alternate_name + = link_to 'Edit', edit_alternate_name_path(@alternate_name), :class => 'btn btn-default btn-xs' +\| += link_to 'Back', alternate_names_path diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index ecaa6b224..83b9ff5c8 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -41,6 +41,11 @@ feature "Alternate names" do href: new_alternate_name_path(crop_id: crop.id) end + scenario "The show-alternate-name page works" do + visit alternate_name_path(alternate_eggplant) + expect(page).to have_content alternate_eggplant.crop.name + end + scenario "The add-alternate-name page works" do crop = alternate_eggplant.crop visit new_alternate_name_path(crop_id: crop.id) From eacfadae20850d18b49a39134c3d02f8a39a68be Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 16:51:43 +0100 Subject: [PATCH 083/132] Make editing alternate names actually do something. --- app/controllers/alternate_names_controller.rb | 16 ++++++++++++++++ spec/features/crop_spec.rb | 3 +++ 2 files changed, 19 insertions(+) diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index af53f9350..857233fa0 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -28,4 +28,20 @@ class AlternateNamesController < ApplicationController format.json { render json: @alternate_name } end end + + # PUT /alternate_names/1 + # PUT /alternate_names/1.json + def update + @alternate_name = AlternateName.find(params[:id]) + + respond_to do |format| + if @alternate_name.update_attributes(params[:alternate_name]) + format.html { redirect_to @alternate_name.crop, notice: 'Alternate name was successfully updated.' } + format.json { head :no_content } + else + format.html { render action: "edit" } + format.json { render json: @alternate_name.errors, status: :unprocessable_entity } + end + end + end end diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 83b9ff5c8..e0f6a6773 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -56,6 +56,9 @@ feature "Alternate names" do crop = alternate_eggplant.crop visit edit_alternate_name_path(alternate_eggplant) expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Name', with: "alternative aubergine" + click_on "Save" + expect(page).to have_content "alternative aubergine" end end From b0096035813b70817921342e92bfe2ef4c415b53 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 16:54:02 +0100 Subject: [PATCH 084/132] Fix link to crop on alternate name page --- app/views/alternate_names/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/alternate_names/show.html.haml b/app/views/alternate_names/show.html.haml index 2f7db7f14..67dfc9a37 100644 --- a/app/views/alternate_names/show.html.haml +++ b/app/views/alternate_names/show.html.haml @@ -5,7 +5,7 @@ = @alternate_name.name %p %b Crop: - = link_to @alternate_name.crop + = link_to @alternate_name.crop, @alternate_name.crop - if can? :edit, @alternate_name = link_to 'Edit', edit_alternate_name_path(@alternate_name), :class => 'btn btn-default btn-xs' From edf2f36bcd48ea10eeb76ccd2ec149ed9309588e Mon Sep 17 00:00:00 2001 From: Cesy Avon Date: Sat, 18 Oct 2014 16:58:23 +0100 Subject: [PATCH 085/132] Upgrading to ruby 2.1.2 --- .ruby-version | 2 +- Gemfile | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ruby-version b/.ruby-version index 3e3c2f1e5..eca07e4c1 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.1.1 +2.1.2 diff --git a/Gemfile b/Gemfile index d463e130e..2360041eb 100644 --- a/Gemfile +++ b/Gemfile @@ -1,6 +1,6 @@ source 'https://rubygems.org' -ruby "2.1.1" +ruby "2.1.2" gem 'bundler', '>=1.1.5' From c7c85aaa664498002928d75e3a0f27995ad1ea3c Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:03:54 +0100 Subject: [PATCH 086/132] Creation and deletion of alternate names --- app/controllers/alternate_names_controller.rb | 30 +++++++++++++++++++ spec/features/crop_spec.rb | 3 ++ 2 files changed, 33 insertions(+) diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index 857233fa0..c7a0ac910 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -29,6 +29,23 @@ class AlternateNamesController < ApplicationController end end + # POST /alternate_names + # POST /alternate_names.json + def create + params[:alternate_name][:creator_id] = current_member.id + @alternate_name = AlternateName.new(params[:alternate_name]) + + respond_to do |format| + if @alternate_name.save + format.html { redirect_to @alternate_name.crop, notice: 'Alternate name was successfully created.' } + format.json { render json: @alternate_name, status: :created, location: @alternate_name } + else + format.html { render action: "new" } + format.json { render json: @alternate_name.errors, status: :unprocessable_entity } + end + end + end + # PUT /alternate_names/1 # PUT /alternate_names/1.json def update @@ -44,4 +61,17 @@ class AlternateNamesController < ApplicationController end end end + + # DELETE /alternate_names/1 + # DELETE /alternate_names/1.json + def destroy + @alternate_name = AlternateName.find(params[:id]) + @crop = @alternate_name.crop + @alternate_name.destroy + + respond_to do |format| + format.html { redirect_to @crop } + format.json { head :no_content } + end + end end diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index e0f6a6773..28fd35f2c 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -50,6 +50,9 @@ feature "Alternate names" do crop = alternate_eggplant.crop visit new_alternate_name_path(crop_id: crop.id) expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Name', with: "not an aubergine" + click_on "Save" + expect(page).to have_content "not an aubergine" end scenario "The edit-alternate-name page works" do From dc2cf5275c7b9ed51fce35e29134416f42e1ed67 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:30:22 +0100 Subject: [PATCH 087/132] Fix model tests for alternate names. --- spec/models/alternate_name_spec.rb | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/spec/models/alternate_name_spec.rb b/spec/models/alternate_name_spec.rb index e7772dca0..f7acd0efc 100644 --- a/spec/models/alternate_name_spec.rb +++ b/spec/models/alternate_name_spec.rb @@ -1,24 +1,22 @@ require 'spec_helper' describe AlternateName do - before (:each) do - @an = FactoryGirl.create(:alternate_tomato) - end + let(:an) { FactoryGirl.create(:alternate_eggplant) } it 'should save a basic alternate name' do - @an.save.should be_true + expect(an.save).to be_true end it 'should be possible to add multiple alternate names to a crop' do - crop = @an.crop + crop = an.crop an2 = AlternateName.create( :name => "really alternative tomato", :crop_id => crop.id, - :creator_id => @an.creator.id + :creator_id => an.creator.id ) crop.alternate_names << an2 - crop.alternate_names.should include @an - crop.alternate_names.should include an2 + expect(crop.alternate_names).to include an + expect(crop.alternate_names).to include an2 end end From 8a97aa1a9bc1170856205469971eb1d78e637f21 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:34:33 +0100 Subject: [PATCH 088/132] Delete redundant old-style test. --- spec/features/crop_spec.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 28fd35f2c..db89af134 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -18,7 +18,6 @@ feature "Alternate names" do fill_in 'Login', with: member.login_name fill_in 'Password', with: member.password click_button 'Sign in' - page.should have_content member.login_name end scenario "Crop wranglers can edit alternate names" do From 96d2fa1cb6739842bb13eee63573c0d9ad48db19 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:41:52 +0100 Subject: [PATCH 089/132] Put alternate names controller in standard order. --- app/controllers/alternate_names_controller.rb | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index c7a0ac910..34aa738d3 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -1,9 +1,15 @@ class AlternateNamesController < ApplicationController load_and_authorize_resource - # GET /alternate_names/1/edit - def edit - @alternate_name = AlternateName.find(params[:id]) + # GET /scientific_names + # GET /scientific_names.json + def index + @scientific_names = ScientificName.all + + respond_to do |format| + format.html # index.html.haml + format.json { render json: @scientific_names } + end end # GET /alternate_names/1 @@ -29,6 +35,11 @@ class AlternateNamesController < ApplicationController end end + # GET /alternate_names/1/edit + def edit + @alternate_name = AlternateName.find(params[:id]) + end + # POST /alternate_names # POST /alternate_names.json def create From 0ae68737c3769babfa12c492aa795e0eac374097 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:43:59 +0100 Subject: [PATCH 090/132] Use login_as helper method in altname feature tests. --- spec/features/crop_spec.rb | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index db89af134..89e81eeb2 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -13,11 +13,7 @@ feature "Alternate names" do let(:member){crop_wranglers.first} before :each do - visit root_path - click_link 'Sign in' - fill_in 'Login', with: member.login_name - fill_in 'Password', with: member.password - click_button 'Sign in' + login_as(member) end scenario "Crop wranglers can edit alternate names" do From c447c1cb3a4abb459e8be0f3f0e1bde001251f5c Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:49:13 +0100 Subject: [PATCH 091/132] Make edit-altname test more featurey. --- app/views/crops/show.html.haml | 50 ++++++++++++++++++---------------- spec/features/crop_spec.rb | 19 +++++-------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index ad5abab1b..2c9413795 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -59,31 +59,33 @@ - if can? :destroy, @crop = link_to 'Delete crop', @crop, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' - %h4 Scientific names - %ul - - @crop.scientific_names.each do |sn| - %li - = sn.scientific_name - - if can? :edit, sn - = link_to 'Edit', edit_scientific_name_path(sn), { :class => 'btn btn-default btn-xs' } - - if can? :destroy, sn - = link_to 'Delete', sn, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' - %p - - if can? :edit, @crop - = link_to 'Add', new_scientific_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } + .scientific_names + %h4 Scientific names + %ul + - @crop.scientific_names.each do |sn| + %li + = sn.scientific_name + - if can? :edit, sn + = link_to 'Edit', edit_scientific_name_path(sn), { :class => 'btn btn-default btn-xs' } + - if can? :destroy, sn + = link_to 'Delete', sn, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' + %p + - if can? :edit, @crop + = link_to 'Add', new_scientific_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } - %h4 Alternate names - %ul - - @crop.alternate_names.each do |an| - %li - = an.name - - if can? :edit, an - = link_to 'Edit', edit_alternate_name_path(an), { :class => 'btn btn-default btn-xs' } - - if can? :destroy, an - = link_to 'Delete', an, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' - %p - - if can? :edit, @crop - = link_to 'Add', new_alternate_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } + .alternate_names + %h4 Alternate names + %ul + - @crop.alternate_names.each do |an| + %li + = an.name + - if can? :edit, an + = link_to 'Edit', edit_alternate_name_path(an), { :class => 'btn btn-default btn-xs' } + - if can? :destroy, an + = link_to 'Delete', an, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' + %p + - if can? :edit, @crop + = link_to 'Add', new_alternate_name_path( :crop_id => @crop.id ), { :class => 'btn btn-default btn-xs' } = render :partial => 'varieties', :locals => { :crop => @crop } diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 89e81eeb2..be8c21b9c 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -2,6 +2,7 @@ require 'spec_helper' feature "Alternate names" do let(:alternate_eggplant) { FactoryGirl.create(:alternate_eggplant) } + let(:crop) { alternate_eggplant.crop } scenario "Display alternate names on crop page" do visit crop_path(alternate_eggplant.crop) @@ -17,10 +18,15 @@ feature "Alternate names" do end scenario "Crop wranglers can edit alternate names" do - visit crop_path(alternate_eggplant.crop) + visit crop_path(crop) expect(page).to have_content "CROP WRANGLER" expect(page).to have_content alternate_eggplant.name expect(page).to have_link "Edit", :href => edit_alternate_name_path(alternate_eggplant) + within('.alternate_names') { click_on "Edit" } + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Name', with: "alternative aubergine" + click_on "Save" + expect(page).to have_content "alternative aubergine" end scenario "Crop wranglers can delete alternate names" do @@ -30,7 +36,6 @@ feature "Alternate names" do end scenario "Crop wranglers can add alternate names" do - crop = alternate_eggplant.crop visit crop_path(crop) expect(page).to have_link "Add", href: new_alternate_name_path(crop_id: crop.id) @@ -42,7 +47,6 @@ feature "Alternate names" do end scenario "The add-alternate-name page works" do - crop = alternate_eggplant.crop visit new_alternate_name_path(crop_id: crop.id) expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" fill_in 'Name', with: "not an aubergine" @@ -50,15 +54,6 @@ feature "Alternate names" do expect(page).to have_content "not an aubergine" end - scenario "The edit-alternate-name page works" do - crop = alternate_eggplant.crop - visit edit_alternate_name_path(alternate_eggplant) - expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" - fill_in 'Name', with: "alternative aubergine" - click_on "Save" - expect(page).to have_content "alternative aubergine" - end - end end From 110ae99d8384e59b0848685cde118c6442fbf98b Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 17:55:56 +0100 Subject: [PATCH 092/132] Fix and test altname index page. --- app/controllers/alternate_names_controller.rb | 8 ++++---- spec/features/crop_spec.rb | 7 ++++++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index 34aa738d3..9d1496e9a 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -1,14 +1,14 @@ class AlternateNamesController < ApplicationController load_and_authorize_resource - # GET /scientific_names - # GET /scientific_names.json + # GET /alternate_names + # GET /alternate_names.json def index - @scientific_names = ScientificName.all + @alternate_names = AlternateName.all respond_to do |format| format.html # index.html.haml - format.json { render json: @scientific_names } + format.json { render json: @alternate_names } end end diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index be8c21b9c..d8e2f31a1 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' feature "Alternate names" do - let(:alternate_eggplant) { FactoryGirl.create(:alternate_eggplant) } + let!(:alternate_eggplant) { FactoryGirl.create(:alternate_eggplant) } let(:crop) { alternate_eggplant.crop } scenario "Display alternate names on crop page" do @@ -9,6 +9,11 @@ feature "Alternate names" do expect(page).to have_content alternate_eggplant.name end + scenario "Index page for alternate names" do + visit alternate_names_path + expect(page).to have_content alternate_eggplant.name + end + context "User is a crop wrangler" do let!(:crop_wranglers) { FactoryGirl.create_list(:crop_wrangling_member, 3) } let(:member){crop_wranglers.first} From 52fd2b86d64b8e8e360d6ec38d3dc1c889f136aa Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 18:01:54 +0100 Subject: [PATCH 093/132] Test altname deletion end-to-end. --- spec/features/crop_spec.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index d8e2f31a1..6cd26b891 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -38,6 +38,8 @@ feature "Alternate names" do visit crop_path(alternate_eggplant.crop) expect(page).to have_link "Delete", href: alternate_name_path(alternate_eggplant) + within('.alternate_names') { click_on "Delete" } + expect(page).to_not have_content alternate_eggplant.name end scenario "Crop wranglers can add alternate names" do From 792062e0d25b7de5a00e2ccb7d0aad156a81f093 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 18:02:59 +0100 Subject: [PATCH 094/132] Make add-altname test more featurey. --- spec/features/crop_spec.rb | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/spec/features/crop_spec.rb b/spec/features/crop_spec.rb index 6cd26b891..3c648f763 100644 --- a/spec/features/crop_spec.rb +++ b/spec/features/crop_spec.rb @@ -46,6 +46,11 @@ feature "Alternate names" do visit crop_path(crop) expect(page).to have_link "Add", href: new_alternate_name_path(crop_id: crop.id) + within('.alternate_names') { click_on "Add" } + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Name', with: "not an aubergine" + click_on "Save" + expect(page).to have_content "not an aubergine" end scenario "The show-alternate-name page works" do @@ -53,14 +58,6 @@ feature "Alternate names" do expect(page).to have_content alternate_eggplant.crop.name end - scenario "The add-alternate-name page works" do - visit new_alternate_name_path(crop_id: crop.id) - expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" - fill_in 'Name', with: "not an aubergine" - click_on "Save" - expect(page).to have_content "not an aubergine" - end - end end From 006bc54f3f935cfb59404ad8d081aa18310a5dba Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sat, 18 Oct 2014 18:13:04 +0100 Subject: [PATCH 095/132] Oops, add the altname index HAML file. --- app/views/alternate_names/index.html.haml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 app/views/alternate_names/index.html.haml diff --git a/app/views/alternate_names/index.html.haml b/app/views/alternate_names/index.html.haml new file mode 100644 index 000000000..60a83bde2 --- /dev/null +++ b/app/views/alternate_names/index.html.haml @@ -0,0 +1,23 @@ +%h1 Listing alternate_names + +- if can? :create, AlternateName + %p= link_to 'New Alternate name', new_alternate_name_path, :class => 'btn btn-primary' + +%table + %tr + %th Alternate name + %th Crop + %th + %th + + - @alternate_names.each do |alternate_name| + %tr + %td= link_to alternate_name.name, alternate_name + %td= alternate_name.crop + %td= link_to 'Show', alternate_name + %td + - if can? :edit, alternate_name + = link_to 'Edit', edit_alternate_name_path(alternate_name), :class => 'btn btn-default btn-xs' + %td + - if can? :destroy, alternate_name + = link_to 'Delete', alternate_name, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' From 1d3c24ae277c14e00aa9aa0a6bc9a20016a96f81 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 11:28:51 +0100 Subject: [PATCH 096/132] Use content_for :title on altname index page. --- app/views/alternate_names/index.html.haml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/views/alternate_names/index.html.haml b/app/views/alternate_names/index.html.haml index 60a83bde2..5cc1f69d0 100644 --- a/app/views/alternate_names/index.html.haml +++ b/app/views/alternate_names/index.html.haml @@ -1,7 +1,7 @@ -%h1 Listing alternate_names +- content_for :title, "Listing alternate names" - if can? :create, AlternateName - %p= link_to 'New Alternate name', new_alternate_name_path, :class => 'btn btn-primary' + %p= link_to 'New alternate name', new_alternate_name_path, :class => 'btn btn-primary' %table %tr From 3a797d4434ada4a34e7f0c98ce9d8826e4867d50 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 11:42:49 +0100 Subject: [PATCH 097/132] Use content_for :title in scientific names index. --- app/views/scientific_names/index.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/scientific_names/index.html.haml b/app/views/scientific_names/index.html.haml index 28cd84710..e0752b156 100644 --- a/app/views/scientific_names/index.html.haml +++ b/app/views/scientific_names/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing scientific_names +- content_for :title, "Listing scientific names" - if can? :create, ScientificName %p= link_to 'New Scientific name', new_scientific_name_path, :class => 'btn btn-primary' From d0f7169c59d9ced87722c88b1ba90abebaed914c Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 11:50:21 +0100 Subject: [PATCH 098/132] Put alternate name features in their own file. --- spec/features/{crop_spec.rb => alternate_name_spec.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/features/{crop_spec.rb => alternate_name_spec.rb} (100%) diff --git a/spec/features/crop_spec.rb b/spec/features/alternate_name_spec.rb similarity index 100% rename from spec/features/crop_spec.rb rename to spec/features/alternate_name_spec.rb From 29d53a8f8b5fcd52c624264afb044103969ec9a6 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 12:02:31 +0100 Subject: [PATCH 099/132] Test for notices in altname CRUD features. --- app/controllers/alternate_names_controller.rb | 4 +++- spec/features/alternate_name_spec.rb | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/controllers/alternate_names_controller.rb b/app/controllers/alternate_names_controller.rb index 9d1496e9a..ea8a8f76c 100644 --- a/app/controllers/alternate_names_controller.rb +++ b/app/controllers/alternate_names_controller.rb @@ -81,7 +81,9 @@ class AlternateNamesController < ApplicationController @alternate_name.destroy respond_to do |format| - format.html { redirect_to @crop } + format.html { + redirect_to @crop, notice: 'Alternate name was successfully deleted.' + } format.json { head :no_content } end end diff --git a/spec/features/alternate_name_spec.rb b/spec/features/alternate_name_spec.rb index 3c648f763..a58e1c365 100644 --- a/spec/features/alternate_name_spec.rb +++ b/spec/features/alternate_name_spec.rb @@ -32,6 +32,7 @@ feature "Alternate names" do fill_in 'Name', with: "alternative aubergine" click_on "Save" expect(page).to have_content "alternative aubergine" + expect(page).to have_content 'Alternate name was successfully updated' end scenario "Crop wranglers can delete alternate names" do @@ -40,6 +41,7 @@ feature "Alternate names" do href: alternate_name_path(alternate_eggplant) within('.alternate_names') { click_on "Delete" } expect(page).to_not have_content alternate_eggplant.name + expect(page).to have_content 'Alternate name was successfully deleted' end scenario "Crop wranglers can add alternate names" do @@ -51,6 +53,7 @@ feature "Alternate names" do fill_in 'Name', with: "not an aubergine" click_on "Save" expect(page).to have_content "not an aubergine" + expect(page).to have_content 'Alternate name was successfully created' end scenario "The show-alternate-name page works" do From 82a61387a3b49bc6630bd7d5b4c6571a3c9773d8 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 12:23:19 +0100 Subject: [PATCH 100/132] Improved layout of member profile page In doing this, I added some content areas for all pages on the site: 1) subtitle 2) buttonbar These are intended to help standardise the layout of all pages. On the member page, the subtitle is the location, and the buttonbar has links like "edit profile", "upgrade account", etc (or if looking at someone else's page, then "send message" etc). I also implemented subtitle/buttonbar on the crop detail page (the subtitle is the default scientific name). The rest is just refactoring and tests. I've removed some view tests and put them in feature tests instead. --- app/assets/stylesheets/overrides.css.less | 15 ++ app/views/crops/show.html.haml | 22 +- app/views/gardens/new.html.haml | 2 +- app/views/layouts/application.html.haml | 13 +- app/views/members/_avatar.html.haml | 2 +- app/views/members/_bio.html.haml | 9 + app/views/members/_contact.html.haml | 17 ++ app/views/members/_gardens.html.haml | 27 +++ app/views/members/_stats.html.haml | 23 ++ app/views/members/show.html.haml | 115 +++------- app/views/scientific_names/index.html.haml | 2 +- spec/features/crop_wranglers_spec.rb | 11 +- spec/features/crops/crop_detail_page_spec.rb | 32 +++ spec/features/member_profile_spec.rb | 132 ++++++++++++ spec/views/crops/show.html.haml_spec.rb | 21 -- spec/views/members/show.html.haml_spec.rb | 214 ------------------- 16 files changed, 308 insertions(+), 349 deletions(-) create mode 100644 app/views/members/_bio.html.haml create mode 100644 app/views/members/_contact.html.haml create mode 100644 app/views/members/_gardens.html.haml create mode 100644 app/views/members/_stats.html.haml create mode 100644 spec/features/crops/crop_detail_page_spec.rb create mode 100644 spec/features/member_profile_spec.rb delete mode 100644 spec/views/members/show.html.haml_spec.rb diff --git a/app/assets/stylesheets/overrides.css.less b/app/assets/stylesheets/overrides.css.less index 7b9952c5d..5826d5c4c 100644 --- a/app/assets/stylesheets/overrides.css.less +++ b/app/assets/stylesheets/overrides.css.less @@ -36,6 +36,21 @@ h2 { font-size: 150%; } +#headingarea { + border-bottom: 1px solid lighten(@brown, 50%) +} + +/* +#subtitle { + color: lighten(@brown, 30%); + margin-top: 0px; + padding-top: 0px; + padding-left: 1em; + font-style: italic; + font-weight: normal; +} +*/ + h3 { font-size: 120%; } diff --git a/app/views/crops/show.html.haml b/app/views/crops/show.html.haml index 670a0d438..c67cb12ce 100644 --- a/app/views/crops/show.html.haml +++ b/app/views/crops/show.html.haml @@ -1,18 +1,18 @@ - content_for :title, @crop.name +- content_for :subtitle, @crop.default_scientific_name +- content_for :buttonbar do + - if can? :create, Planting + = link_to "Plant this", new_planting_path(:crop_id => @crop.id), :class => 'btn btn-default' + + - if can? :create, Harvest + = link_to "Harvest this", new_harvest_path(:crop_id => @crop.id), :class => 'btn btn-default' + + - if can? :create, Seed + = link_to 'Add seeds to stash', new_seed_path(:params => { :crop_id => @crop.id }), :class => 'btn btn-default' + .row .col-md-9 - %p - - if can? :create, Planting - = link_to "Plant this", new_planting_path(:crop_id => @crop.id), :class => 'btn btn-primary' - - else - = render :partial => 'shared/signin_signup', :locals => { :to => 'plant this crop' } - - - if can? :create, Harvest - = link_to "Harvest this", new_harvest_path(:crop_id => @crop.id), :class => 'btn btn-primary' - - - if can? :create, Seed - = link_to 'Add seeds to stash', new_seed_path(:params => { :crop_id => @crop.id }), :class => 'btn btn-primary' = render :partial => 'photos', :locals => { :crop => @crop } diff --git a/app/views/gardens/new.html.haml b/app/views/gardens/new.html.haml index 3e7ee5f67..4fdc839b1 100644 --- a/app/views/gardens/new.html.haml +++ b/app/views/gardens/new.html.haml @@ -1,3 +1,3 @@ -%h1 New garden +- content_for :title, "New garden" = render 'form' diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 7ff38d7c2..559daa608 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -8,8 +8,17 @@ .container .row .col-md-12 - - if content_for?(:title) - %h1= yield(:title) + #headingarea + - if content_for?(:title) + %h1#title + = yield(:title) + - if content_for?(:subtitle) + %small= yield(:subtitle) + + - if content_for?(:buttonbar) + %p + .btn-group + = yield(:buttonbar) - if notice .alert.alert-success = notice diff --git a/app/views/members/_avatar.html.haml b/app/views/members/_avatar.html.haml index c56c8768a..25c0e3140 100644 --- a/app/views/members/_avatar.html.haml +++ b/app/views/members/_avatar.html.haml @@ -5,5 +5,5 @@ :size => defined?(size) ? size : 150, | :default => :identicon }), | :alt => '', | - :class => 'img-rounded img-responsive' ), | + :class => 'img-rounded img-responsive avatar' ), | member_path(member) diff --git a/app/views/members/_bio.html.haml b/app/views/members/_bio.html.haml new file mode 100644 index 000000000..3858d224c --- /dev/null +++ b/app/views/members/_bio.html.haml @@ -0,0 +1,9 @@ +%h2 All about #{member.login_name} +- if member.bio.blank? + - if can? :edit, member + = link_to "Add a bio to complete your profile." + - else + #{member.login_name} hasn't written a bio yet. +- else + :growstuff_markdown + #{ strip_tags member.bio } diff --git a/app/views/members/_contact.html.haml b/app/views/members/_contact.html.haml new file mode 100644 index 000000000..e5d24327b --- /dev/null +++ b/app/views/members/_contact.html.haml @@ -0,0 +1,17 @@ +- if twitter_auth || flickr_auth || member.show_email + %h4 Contact + + - if twitter_auth + %p + = image_tag "twitter_32.png", :size => "32x32", :alt => 'Twitter logo' + =link_to twitter_auth.name, "http://twitter.com/#{twitter_auth.name}" + + - if flickr_auth + %p + = image_tag "flickr_32.png", :size => "32x32", :alt => 'Flickr logo' + =link_to flickr_auth.name, "http://flickr.com/photos/#{flickr_auth.uid}" + + - if member.show_email + %p + Email: + = mail_to member.email diff --git a/app/views/members/_gardens.html.haml b/app/views/members/_gardens.html.haml new file mode 100644 index 000000000..89ed8f83e --- /dev/null +++ b/app/views/members/_gardens.html.haml @@ -0,0 +1,27 @@ +%h2 #{member.login_name}'s gardens +.tabbable + %ul.nav.nav-tabs + - first_garden = true + - member.gardens.each do |g| + %li{:class => first_garden ? 'active' : '' } + - first_garden = false + = link_to g.name, "#garden#{g.id}", 'data-toggle' => 'tab' + - if current_member == member + %li= link_to 'New Garden', new_garden_path + .tab-content + - first_garden = true + - member.gardens.each do |g| + + %div{:class => ['tab-pane', first_garden ? 'active' : ''], :id => "garden#{g.id}"} + - first_garden = false + + %div + :growstuff_markdown + #{ strip_tags g.description } + + %h3 What's planted here? + - g.featured_plantings.each do |p| + = render :partial => "plantings/thumbnail", :locals => { :planting => p, :hide_description => true } + + %p + = link_to "More about this garden...", url_for(g) diff --git a/app/views/members/_stats.html.haml b/app/views/members/_stats.html.haml new file mode 100644 index 000000000..38d6b96c3 --- /dev/null +++ b/app/views/members/_stats.html.haml @@ -0,0 +1,23 @@ +%h3 Activity + +%ul + %li + - if member.plantings.count > 0 + = link_to pluralize(member.plantings.count, "planting"), plantings_by_owner_path(:owner => member) + - else + 0 plantings + %li + - if member.harvests.count > 0 + = link_to pluralize(member.harvests.count, "harvest"), harvests_by_owner_path(:owner => member) + - else + 0 harvests + %li + - if member.seeds.count > 0 + = link_to pluralize(member.seeds.count, "seeds"), seeds_by_owner_path(:owner => member) + - else + 0 seeds + %li + - if member.posts.count > 0 + = link_to pluralize(member.posts.count, "post"), posts_by_author_path(:author => member) + - else + 0 posts diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index acbcb8c34..317caae1b 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -1,15 +1,28 @@ -- content_for :title, "#{@member.login_name}" -- content_for :member_rss_login_name, "#{@member.login_name}" -- content_for :member_rss_slug, "#{@member.slug}" +- content_for :title, @member.login_name +- content_for :subtitle, @member.location +- content_for :buttonbar do + - if can? :update, @member + = link_to 'Edit profile', edit_member_registration_path, :class => 'btn btn-default' + - if @member == current_member && !@member.is_paid? + = link_to "Upgrade account", shop_path, :class => 'btn btn-default' + -if can? :create, Notification and current_member != @member + =link_to 'Send message', new_notification_path(:recipient_id => @member.id), :class => 'btn btn-default' + +- content_for :member_rss_login_name, @member.login_name +- content_for :member_rss_slug, @member.slug .row - .col-md-3 - = render :partial => "members/avatar", :locals => { :member => @member } - -if can? :create, Notification and current_member != @member - %p - %br/ - =link_to 'Send Message', new_notification_path(:recipient_id => @member.id), :class => 'btn btn-primary' + .col-md-9 + + = render :partial => "bio", :locals => { :member => @member } + + = render :partial => "gardens", :locals => { :member => @member } + + .col-md-3 + = render :partial => "avatar", :locals => { :member => @member } + + %h3 Account details %p %strong Member since: @@ -20,86 +33,6 @@ = @member.account_type account - - if @member == current_member && !@member.is_paid? - = link_to "Upgrade", shop_path, :class => 'btn btn-primary btn-xs' + = render :partial => "contact", :locals => { :member => @member, :twitter_auth => @twitter_auth, :flickr_auth => @flickr_auth } - - if @twitter_auth || @flickr_auth || @member.show_email - %h4 Contact - - - if @twitter_auth - %p - = image_tag "twitter_32.png", :size => "32x32", :alt => 'Twitter logo' - =link_to @twitter_auth.name, "http://twitter.com/#{@twitter_auth.name}" - - - if @flickr_auth - %p - = image_tag "flickr_32.png", :size => "32x32", :alt => 'Flickr logo' - =link_to @flickr_auth.name, "http://flickr.com/photos/#{@flickr_auth.uid}" - - - if @member.show_email - %p - Email: - = mail_to @member.email - - - if @member.location.to_s != '' - %h4 Location - %p - = link_to image_tag("http://maps.google.com/maps/api/staticmap?size=200x200&maptype=roadmap&sensor=false&markers=color:green|label:A|#{@member.latitude},#{@member.longitude}&zoom=12", :alt => "Map showing #{@member.location}", :width => 200, :height => 200 ), place_path(@member.location) - %br/ - = link_to @member.location, place_path(@member.location) - - .col-md-9 - %p - - if can? :update, @member - %p - = link_to 'Edit your profile', "edit", :class => 'btn btn-primary' - - if @member.bio - %h2 Bio - :growstuff_markdown - #{ strip_tags @member.bio } - %h2 Gardens - .tabbable - %ul.nav.nav-tabs - - first_garden = true - - @member.gardens.each do |g| - %li{:class => first_garden ? 'active' : '' } - - first_garden = false - = link_to g.name, "#garden#{g.id}", 'data-toggle' => 'tab' - - if current_member == @member - %li= link_to 'New Garden', '#garden_new', 'data-toggle' => 'tab' - .tab-content - - first_garden = true - - @member.gardens.each do |g| - - %div{:class => ['tab-pane', first_garden ? 'active' : ''], :id => "garden#{g.id}"} - - first_garden = false - - %div - :growstuff_markdown - #{ strip_tags g.description } - - %h3 What's planted here? - - g.featured_plantings.each do |p| - = render :partial => "plantings/thumbnail", :locals => { :planting => p, :hide_description => true } - - %p - = link_to "More about this garden...", url_for(g) - - - if current_member == @member - %div{:class => 'tab-pane', :id => "garden_new"} - %h3 Create a new garden - = render 'gardens/form' - - %h3 Seeds - %p - - if @member.seeds.count > 0 - = link_to pluralize(@member.seeds.count, "variety"), seeds_path(:owner_id => @member.id) - - else - No seeds yet. - - %h3 Posts - - if @member.posts.count > 0 - - @member.posts.each do |post| - = render :partial => "posts/single", :locals => { :post => post, :subject => true } - - else - %p Nothing posted yet. + = render :partial => "stats", :locals => { :member => @member } diff --git a/app/views/scientific_names/index.html.haml b/app/views/scientific_names/index.html.haml index 28cd84710..8a5ae901d 100644 --- a/app/views/scientific_names/index.html.haml +++ b/app/views/scientific_names/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing scientific_names +-content_for :title, "Listing scientific_names" - if can? :create, ScientificName %p= link_to 'New Scientific name', new_scientific_name_path, :class => 'btn btn-primary' diff --git a/spec/features/crop_wranglers_spec.rb b/spec/features/crop_wranglers_spec.rb index 80475bd18..32c96e639 100644 --- a/spec/features/crop_wranglers_spec.rb +++ b/spec/features/crop_wranglers_spec.rb @@ -4,16 +4,13 @@ feature "crop wranglers" do context "signed in member" do let!(:crop_wranglers) { FactoryGirl.create_list(:crop_wrangling_member, 3) } let(:member){crop_wranglers.first} - before :each do - visit root_path - click_link 'Sign in' - fill_in 'Login', with: member.login_name - fill_in 'Password', with: member.password - click_button 'Sign in' - page.should have_content member.login_name + + background do + login_as(member) end scenario "crop wranglers are listed on the crop wrangler page" do + visit root_path click_link 'Crop Wrangling' within '.crop_wranglers' do diff --git a/spec/features/crops/crop_detail_page_spec.rb b/spec/features/crops/crop_detail_page_spec.rb new file mode 100644 index 000000000..a941cb075 --- /dev/null +++ b/spec/features/crops/crop_detail_page_spec.rb @@ -0,0 +1,32 @@ +require 'spec_helper' + +feature "crop detail page" do + + let(:crop) { FactoryGirl.create(:crop) } + + context "signed in member" do + let(:member) { FactoryGirl.create(:member) } + + background do + login_as(member) + end + + context "action buttons" do + + background do + visit crop_path(crop) + end + + scenario "has a link to plant the crop" do + expect(page).to have_link "Plant this", :href => new_planting_path(:crop_id => crop.id) + end + scenario "has a link to harvest the crop" do + expect(page).to have_link "Harvest this", :href => new_harvest_path(:crop_id => crop.id) + end + scenario "has a link to add seeds" do + expect(page).to have_link "Add seeds to stash", :href => new_seed_path(:crop_id => crop.id) + end + + end + end +end diff --git a/spec/features/member_profile_spec.rb b/spec/features/member_profile_spec.rb new file mode 100644 index 000000000..88f29548a --- /dev/null +++ b/spec/features/member_profile_spec.rb @@ -0,0 +1,132 @@ +require 'spec_helper' + +feature "member profile" do + + context "signed out member" do + let(:member) { FactoryGirl.create(:member) } + + scenario "basic details on member profile page" do + visit member_path(member) + expect(page).to have_css("h1", :text => member.login_name) + expect(page).to have_content member.bio + expect(page).to have_content "Member since: #{member.created_at.to_s(:date)}" + expect(page).to have_content "Account type: Free account" + expect(page).to have_content "#{member.login_name}'s gardens" + expect(page).to have_link "More about this garden...", :href => garden_path(member.gardens.first) + end + + scenario "no bio" do + member.bio = nil + member.save + visit member_path(member) + expect(page).to have_content "hasn't written a bio yet" + end + + scenario "gravatar" do + visit member_path(member) + expect(page).to have_css "img.avatar" + end + + context "location" do + scenario "member has set location" do + london_member = FactoryGirl.create(:london_member) + visit member_path(london_member) + expect(page).to have_css("h1>small", :text => london_member.location) + end + + scenario "member has not set location" do + visit member_path(member) + expect(page).not_to have_css("h1>small") + end + end + + context "email privacy" do + scenario "public email address" do + public_member = FactoryGirl.create(:public_member) + visit member_path(public_member) + expect(page).to have_content public_member.email + end + scenario "private email address" do + visit member_path(member) + expect(page).not_to have_content member.email + end + end + + context "activity stats" do + + scenario "with no activity" do + visit member_path(member) + expect(page).to have_content "Activity" + expect(page).to have_content "0 plantings" + expect(page).to have_content "0 harvests" + expect(page).to have_content "0 seeds" + expect(page).to have_content "0 posts" + end + + scenario "with some activity" do + FactoryGirl.create_list(:planting, 2, :owner => member) + FactoryGirl.create_list(:harvest, 3, :owner => member) + FactoryGirl.create_list(:seed, 4, :owner => member) + FactoryGirl.create_list(:post, 5, :author => member) + visit member_path(member) + expect(page).to have_link "2 plantings", :href => plantings_by_owner_path(:owner => member) + expect(page).to have_link "3 harvests", :href => harvests_by_owner_path(:owner => member) + expect(page).to have_link "4 seeds", :href => seeds_by_owner_path(:owner => member) + expect(page).to have_link "5 posts", :href => posts_by_author_path(:author => member) + end + + end + + scenario "twitter link" do + twitter_auth = FactoryGirl.create(:authentication, :member => member) + visit member_path(member) + expect(page).to have_link twitter_auth.name, :href => "http://twitter.com/#{twitter_auth.name}" + end + + scenario "flickr link" do + flickr_auth = FactoryGirl.create(:flickr_authentication, :member => member) + visit member_path(member) + expect(page).to have_link flickr_auth.name, :href => "http://flickr.com/photos/#{flickr_auth.uid}" + end + + end + + context "signed in member" do + let(:member) { FactoryGirl.create(:member) } + let(:other_member) { FactoryGirl.create(:member) } + + background do + login_as(member) + end + + context "your own profile page" do + background do + visit member_path(member) + end + + scenario "has a link to create new garden" do + expect(page).to have_link "New Garden", :href => new_garden_path + end + + scenario "has a button to edit profile" do + expect(page).to have_link "Edit profile", :href => edit_member_registration_path + end + + scenario "has a button to upgrade account" do + expect(page).to have_link "Upgrade account", :href => shop_path + end + + end + + context "someone else's profile page" do + background do + visit member_path(other_member) + end + + scenario "has a private message button" do + expect(page).to have_link "Send message", :href => new_notification_path(:recipient_id => other_member.id) + end + end + + end +end diff --git a/spec/views/crops/show.html.haml_spec.rb b/spec/views/crops/show.html.haml_spec.rb index 1ef461508..ca6166721 100644 --- a/spec/views/crops/show.html.haml_spec.rb +++ b/spec/views/crops/show.html.haml_spec.rb @@ -185,11 +185,6 @@ describe "crops/show" do end - it 'tells you to sign in/sign up' do - render - rendered.should contain 'Sign in or sign up to plant' - end - context 'logged in' do before(:each) do @member = FactoryGirl.create(:member) @@ -198,22 +193,6 @@ describe "crops/show" do render end - it "shows a plant this button" do - rendered.should contain "Plant this" - end - - it "shows a harvest this button" do - rendered.should contain "Harvest this" - end - - it "links to the right crop in the new planting link" do - assert_select("a[href=#{new_planting_path}?crop_id=#{@crop.id}]") - end - - it "links to the right crop in the new harvest link" do - assert_select("a[href=#{new_harvest_path}?crop_id=#{@crop.id}]") - end - it { rendered.should contain "Nobody has planted this crop yet" } it { rendered.should contain "Nobody has harvested this crop yet" } diff --git a/spec/views/members/show.html.haml_spec.rb b/spec/views/members/show.html.haml_spec.rb deleted file mode 100644 index f6c0279d6..000000000 --- a/spec/views/members/show.html.haml_spec.rb +++ /dev/null @@ -1,214 +0,0 @@ -require 'spec_helper' - -describe "members/show" do - before(:each) do - controller.stub(:current_user) { nil } - @member = FactoryGirl.create(:member) - @garden = FactoryGirl.create(:garden, :owner => @member) - end - - context "the basics" do - before(:each) do - render - end - - it "shows the bio" do - assert_select "h2", "Bio" - rendered.should contain @member.bio - end - - it "shows account creation date" do - @time = @member.created_at - rendered.should contain "Member since" - rendered.should contain @time.strftime("%B %d, %Y") - end - - it "shows account type" do - rendered.should contain "Free account" - end - - it "contains a gravatar icon" do - assert_select "img", :src => /gravatar\.com\/avatar/ - end - end - - context 'no bio' do - before(:each) do - @member = FactoryGirl.create(:no_bio_member) - render - end - - it "doesn't show the bio" do - rendered.should_not contain "Bio" - end - end - - context 'twitter' do - context "no twitter" do - it "doesn't show twitter link" do - render - assert_select "a[href^=http://twitter.com/]", :count => 0 - end - end - context 'has twitter' do - it "shows twitter link" do - @twitter_auth = FactoryGirl.create(:authentication, :member => @member) - render - assert_select "a", :href => "http://twitter.com/#{@twitter_auth.name}" - end - end - end - - context 'flickr' do - context "no flickr" do - it "doesn't show flickr link" do - render - assert_select "a[href^=http://flickr.com/]", :count => 0 - end - end - context 'has flickr' do - it "shows flickr link" do - @flickr_auth = FactoryGirl.create(:flickr_authentication, :member => @member) - render - assert_select "a", :href => "http://flickr.com/photos/#{@flickr_auth.uid}" - end - end - end - - context "gardens and plantings" do - before(:each) do - @planting = FactoryGirl.create(:planting, :garden => @garden) - render - end - - it "shows the auto-created garden" do - assert_select "li.active>a", :text => "Garden" - end - - it 'shows the garden description' do - rendered.should contain "totally cool garden" - end - - it 'renders markdown in the garden description' do - assert_select "strong", "totally" - end - - it "shows the plantings in the garden" do - rendered.should contain @planting.crop.name - end - - it "doesn't show the note about random plantings" do - rendered.should_not contain "Note: these are a random selection" - end - - it "doesn't show the email address" do - rendered.should_not contain @member.email - end - - it "does not contain a 'New Garden' link" do - assert_select "a[href=#garden_new]", false - end - - it "does not contain a 'New Garden' tab" do - assert_select "#garden_new", false - end - end - - context "signed in member" do - before(:each) do - sign_in @member - controller.stub(:current_user) { @member } - render - end - - it "contains a 'New Garden' link" do - assert_select "a[href=#garden_new]", :text => "New Garden" - end - - it "contains an edit settings button" do - rendered.should contain "Edit your profile" - end - - it "asks you to upgrade your account" do - rendered.should contain "Upgrade" - end - - it "contains no send message button" do - rendered.should_not contain "Send Message" - end - end - - context "signed in as different member" do - before(:each) do - @member2 = FactoryGirl.create(:member) - sign_in @member2 - controller.stub(:current_user) { @member2 } - render - end - - it "does not contain a 'New Garden' link" do - assert_select "a[href=#garden_new]", false - end - - it "does not contain a 'New Garden' tab" do - assert_select "#garden_new", false - end - - it "contains no edit settings button" do - rendered.should_not contain "Edit Settings" - end - - it "contains a send message button" do - rendered.should contain "Send Message" - end - end - - context "public member" do - before(:each) do - @member = FactoryGirl.create(:public_member) - render - end - - it "shows the email address" do - rendered.should contain @member.email - end - - it "doesn't show a send message button" do - rendered.should_not contain "Send Message" - end - end - - context "geolocations" do - before(:each) do - @member = FactoryGirl.create(:london_member) - render - end - it "shows the location" do - rendered.should contain @member.location - end - it "shows a map" do - assert_select "img", :src => /maps\.google\.com/ - end - - it 'includes a link to places page' do - assert_select 'a', :href => place_path(@member.location) - end - end - - context "no location stated" do - before(:each) do - @member = FactoryGirl.create(:member) - render - end - it "doesn't have a location" do - @member.location.to_s.should eq '' - end - it "doesn't show the location" do - rendered.should_not contain "Location:" - end - it "doesn't show a map" do - assert_select "img[src*=maps]", false - end - end - -end From e613907506c39491bb14cbb8246c5841621f3a7e Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 13:21:30 +0100 Subject: [PATCH 101/132] Check status codes on page visits. --- spec/features/alternate_name_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/features/alternate_name_spec.rb b/spec/features/alternate_name_spec.rb index a58e1c365..086584c40 100644 --- a/spec/features/alternate_name_spec.rb +++ b/spec/features/alternate_name_spec.rb @@ -6,6 +6,7 @@ feature "Alternate names" do scenario "Display alternate names on crop page" do visit crop_path(alternate_eggplant.crop) + expect(page.status_code).to equal 200 expect(page).to have_content alternate_eggplant.name end @@ -24,13 +25,16 @@ feature "Alternate names" do scenario "Crop wranglers can edit alternate names" do visit crop_path(crop) + expect(page.status_code).to equal 200 expect(page).to have_content "CROP WRANGLER" expect(page).to have_content alternate_eggplant.name expect(page).to have_link "Edit", :href => edit_alternate_name_path(alternate_eggplant) within('.alternate_names') { click_on "Edit" } + expect(page.status_code).to equal 200 expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" fill_in 'Name', with: "alternative aubergine" click_on "Save" + expect(page.status_code).to equal 200 expect(page).to have_content "alternative aubergine" expect(page).to have_content 'Alternate name was successfully updated' end @@ -40,6 +44,7 @@ feature "Alternate names" do expect(page).to have_link "Delete", href: alternate_name_path(alternate_eggplant) within('.alternate_names') { click_on "Delete" } + expect(page.status_code).to equal 200 expect(page).to_not have_content alternate_eggplant.name expect(page).to have_content 'Alternate name was successfully deleted' end @@ -49,15 +54,18 @@ feature "Alternate names" do expect(page).to have_link "Add", href: new_alternate_name_path(crop_id: crop.id) within('.alternate_names') { click_on "Add" } + expect(page.status_code).to equal 200 expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" fill_in 'Name', with: "not an aubergine" click_on "Save" + expect(page.status_code).to equal 200 expect(page).to have_content "not an aubergine" expect(page).to have_content 'Alternate name was successfully created' end scenario "The show-alternate-name page works" do visit alternate_name_path(alternate_eggplant) + expect(page.status_code).to equal 200 expect(page).to have_content alternate_eggplant.crop.name end From 41566d39f31fc3a6592ede74ae7fb6ecd856e4fa Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 13:50:43 +0100 Subject: [PATCH 102/132] Added map to member profile page --- app/assets/javascripts/members.js.erb | 30 +++++++++++++++++++++++ app/assets/stylesheets/overrides.css.less | 4 +++ app/controllers/members_controller.rb | 2 ++ app/views/members/_account.html.haml | 12 +++++++++ app/views/members/_map.html.haml | 5 ++++ app/views/members/show.html.haml | 16 ++---------- spec/features/member_profile_spec.rb | 17 +++++++++++++ 7 files changed, 72 insertions(+), 14 deletions(-) create mode 100644 app/assets/javascripts/members.js.erb create mode 100644 app/views/members/_account.html.haml create mode 100644 app/views/members/_map.html.haml diff --git a/app/assets/javascripts/members.js.erb b/app/assets/javascripts/members.js.erb new file mode 100644 index 000000000..a3f29904d --- /dev/null +++ b/app/assets/javascripts/members.js.erb @@ -0,0 +1,30 @@ +if (document.getElementById("membermap") !== null) { + mapbox_map_id = "<%= Rails.env == 'test' ? 0 : Growstuff::Application.config.mapbox_map_id %>"; + mapbox_base_url = "https://c.tiles.mapbox.com/v3/" + mapbox_map_id + "/{z}/{x}/{y}.png"; + + L.Icon.Default.imagePath = '/assets' + + + $.getJSON(location.pathname + '.json', function(member) { + console.log(JSON.stringify(member.latitude)); + if (member.latitude && member.longitude) { + membermap = L.map('membermap').setView([member.latitude, member.longitude], 4); + + L.tileLayer(mapbox_base_url, { + attribution: 'Map data © OpenStreetMap contributors under ODbL | Map imagery © Mapbox', + maxZoom: 18 + }).addTo(membermap); + console.log("found lat and long") + marker = new L.Marker(new L.LatLng(member.latitude, member.longitude)); + + member_url = "/members/" + member.slug; + member_link = "" + member.login_name + ""; + + where = "

" + member.location + "

"; + + marker.bindPopup(member_link + where).openPopup(); + marker.addTo(membermap); + } + }); + +} diff --git a/app/assets/stylesheets/overrides.css.less b/app/assets/stylesheets/overrides.css.less index 5826d5c4c..a4ac0587f 100644 --- a/app/assets/stylesheets/overrides.css.less +++ b/app/assets/stylesheets/overrides.css.less @@ -113,6 +113,10 @@ p.stats { height: 500px; } +#membermap { + height: 250px; +} + .member-location { font-size: small; font-style: italic; diff --git a/app/controllers/members_controller.rb b/app/controllers/members_controller.rb index ab3de0001..247ac1c8b 100644 --- a/app/controllers/members_controller.rb +++ b/app/controllers/members_controller.rb @@ -10,6 +10,7 @@ class MembersController < ApplicationController respond_to do |format| format.html # index.html.haml + format.json { render :json => @members.to_json(:only => [:id, :login_name, :slug, :bio, :created_at, :location, :latitude, :longitude]) } end end @@ -25,6 +26,7 @@ class MembersController < ApplicationController respond_to do |format| format.html # show.html.haml + format.json { render :json => @member.to_json(:only => [:id, :login_name, :bio, :created_at, :slug, :location, :latitude, :longitude]) } format.rss { render( :layout => false, :locals => { :member => @member } diff --git a/app/views/members/_account.html.haml b/app/views/members/_account.html.haml new file mode 100644 index 000000000..37ff0c699 --- /dev/null +++ b/app/views/members/_account.html.haml @@ -0,0 +1,12 @@ +%h3 Account details + +%p + %strong Member since: + = member.created_at.to_s(:date) + +%p + %strong Account type: + = member.account_type + account + + diff --git a/app/views/members/_map.html.haml b/app/views/members/_map.html.haml new file mode 100644 index 000000000..d98792501 --- /dev/null +++ b/app/views/members/_map.html.haml @@ -0,0 +1,5 @@ +- if member.latitude and member.longitude + %div#membermap + %p + See other members near + = link_to member.location, place_path(member.location) diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index 317caae1b..edf4d0b68 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -15,24 +15,12 @@ .col-md-9 + = render :partial => "map", :locals => { :member => @member } = render :partial => "bio", :locals => { :member => @member } - = render :partial => "gardens", :locals => { :member => @member } .col-md-3 = render :partial => "avatar", :locals => { :member => @member } - - %h3 Account details - - %p - %strong Member since: - = @member.created_at.to_s(:date) - - %p - %strong Account type: - = @member.account_type - account - + = render :partial => "account", :locals => { :member => @member } = render :partial => "contact", :locals => { :member => @member, :twitter_auth => @twitter_auth, :flickr_auth => @flickr_auth } - = render :partial => "stats", :locals => { :member => @member } diff --git a/spec/features/member_profile_spec.rb b/spec/features/member_profile_spec.rb index 88f29548a..809b052a8 100644 --- a/spec/features/member_profile_spec.rb +++ b/spec/features/member_profile_spec.rb @@ -32,11 +32,28 @@ feature "member profile" do london_member = FactoryGirl.create(:london_member) visit member_path(london_member) expect(page).to have_css("h1>small", :text => london_member.location) + expect(page).to have_css("#membermap") + expect(page).to have_content "See other members near #{london_member.location}" end scenario "member has not set location" do visit member_path(member) expect(page).not_to have_css("h1>small") + expect(page).not_to have_css("#membermap") + expect(page).not_to have_content "See other members near" + end + + end + + context "email privacy" do + scenario "public email address" do + public_member = FactoryGirl.create(:public_member) + visit member_path(public_member) + expect(page).to have_content public_member.email + end + scenario "private email address" do + visit member_path(member) + expect(page).not_to have_content member.email end end From 16677a3b8637e09c348409f97ec2f5188fd26abc Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 14:04:32 +0100 Subject: [PATCH 103/132] Removed underline on header area --- app/assets/stylesheets/overrides.css.less | 4 ---- app/views/layouts/application.html.haml | 11 +++++------ 2 files changed, 5 insertions(+), 10 deletions(-) diff --git a/app/assets/stylesheets/overrides.css.less b/app/assets/stylesheets/overrides.css.less index a4ac0587f..8b9938c33 100644 --- a/app/assets/stylesheets/overrides.css.less +++ b/app/assets/stylesheets/overrides.css.less @@ -36,10 +36,6 @@ h2 { font-size: 150%; } -#headingarea { - border-bottom: 1px solid lighten(@brown, 50%) -} - /* #subtitle { color: lighten(@brown, 30%); diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 559daa608..a3e1fd1d5 100644 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -8,12 +8,11 @@ .container .row .col-md-12 - #headingarea - - if content_for?(:title) - %h1#title - = yield(:title) - - if content_for?(:subtitle) - %small= yield(:subtitle) + - if content_for?(:title) + %h1#title + = yield(:title) + - if content_for?(:subtitle) + %small= yield(:subtitle) - if content_for?(:buttonbar) %p From f468a8b77b9b55a6e5255bab0a2f25376b45c0c8 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 14:04:55 +0100 Subject: [PATCH 104/132] Changed all h1 to content_for :title --- app/views/account_types/edit.html.haml | 2 +- app/views/account_types/index.html.haml | 2 +- app/views/account_types/new.html.haml | 2 +- app/views/accounts/edit.html.haml | 2 +- app/views/accounts/index.html.haml | 2 +- app/views/accounts/new.html.haml | 2 +- app/views/authentications/index.html.haml | 2 +- app/views/plant_parts/edit.html.haml | 2 +- app/views/plant_parts/new.html.haml | 2 +- app/views/products/edit.html.haml | 2 +- app/views/products/index.html.haml | 2 +- app/views/products/new.html.haml | 2 +- app/views/roles/edit.html.haml | 2 +- app/views/roles/index.html.haml | 2 +- app/views/roles/new.html.haml | 2 +- 15 files changed, 15 insertions(+), 15 deletions(-) diff --git a/app/views/account_types/edit.html.haml b/app/views/account_types/edit.html.haml index e8d328a21..79f11726d 100644 --- a/app/views/account_types/edit.html.haml +++ b/app/views/account_types/edit.html.haml @@ -1,4 +1,4 @@ -%h1 Editing account_type +-content_for :title, "Editing account type" = render 'form' diff --git a/app/views/account_types/index.html.haml b/app/views/account_types/index.html.haml index a27434af4..5fc8a3f05 100644 --- a/app/views/account_types/index.html.haml +++ b/app/views/account_types/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing account_types +- content_for :title, "Listing account types" %table %tr diff --git a/app/views/account_types/new.html.haml b/app/views/account_types/new.html.haml index bf85a1baf..92d309715 100644 --- a/app/views/account_types/new.html.haml +++ b/app/views/account_types/new.html.haml @@ -1,4 +1,4 @@ -%h1 New account_type +- content_for :title, "New account type" = render 'form' diff --git a/app/views/accounts/edit.html.haml b/app/views/accounts/edit.html.haml index 5b4039cfa..ef8d08801 100644 --- a/app/views/accounts/edit.html.haml +++ b/app/views/accounts/edit.html.haml @@ -1,4 +1,4 @@ -%h1 Editing account +- content_for :title, "Editing account" = render 'form' diff --git a/app/views/accounts/index.html.haml b/app/views/accounts/index.html.haml index 66345747f..1dfef6438 100644 --- a/app/views/accounts/index.html.haml +++ b/app/views/accounts/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing accounts +- content_for :title, "Listing accounts" %table %tr diff --git a/app/views/accounts/new.html.haml b/app/views/accounts/new.html.haml index 489317b27..9430cc2ed 100644 --- a/app/views/accounts/new.html.haml +++ b/app/views/accounts/new.html.haml @@ -1,4 +1,4 @@ -%h1 New account +- content_for :title, "New account" = render 'form' diff --git a/app/views/authentications/index.html.haml b/app/views/authentications/index.html.haml index 61cd83250..3bb1607f1 100644 --- a/app/views/authentications/index.html.haml +++ b/app/views/authentications/index.html.haml @@ -1,4 +1,4 @@ -%h1 Linked accounts on other sites +- content_for :title, "Linked accounts on other sites" - if @authentications - unless @authentications.empty? diff --git a/app/views/plant_parts/edit.html.haml b/app/views/plant_parts/edit.html.haml index 6add25fb3..f17252ef8 100644 --- a/app/views/plant_parts/edit.html.haml +++ b/app/views/plant_parts/edit.html.haml @@ -1,4 +1,4 @@ -%h1 Editing plant_part +- content_for :title, "Editing plant part" = render 'form' diff --git a/app/views/plant_parts/new.html.haml b/app/views/plant_parts/new.html.haml index 604c38a04..e0d446dcb 100644 --- a/app/views/plant_parts/new.html.haml +++ b/app/views/plant_parts/new.html.haml @@ -1,4 +1,4 @@ -%h1 New plant_part +- content_for :title, "New plant part" = render 'form' diff --git a/app/views/products/edit.html.haml b/app/views/products/edit.html.haml index 9c16087ea..84ebcb870 100644 --- a/app/views/products/edit.html.haml +++ b/app/views/products/edit.html.haml @@ -1,4 +1,4 @@ -%h1 Editing product +- content_for :title, "Editing product" = render 'form' diff --git a/app/views/products/index.html.haml b/app/views/products/index.html.haml index 61940aaa8..17053aeca 100644 --- a/app/views/products/index.html.haml +++ b/app/views/products/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing products +- content_for :title, "Listing products" %table %tr diff --git a/app/views/products/new.html.haml b/app/views/products/new.html.haml index 71ed863eb..57b944d9a 100644 --- a/app/views/products/new.html.haml +++ b/app/views/products/new.html.haml @@ -1,4 +1,4 @@ -%h1 New product +- content_for :title, "New product" = render 'form' diff --git a/app/views/roles/edit.html.haml b/app/views/roles/edit.html.haml index 427975d14..83520111b 100644 --- a/app/views/roles/edit.html.haml +++ b/app/views/roles/edit.html.haml @@ -1,4 +1,4 @@ -%h1 Editing role +- content_for :title, "Editing role" = render 'form' diff --git a/app/views/roles/index.html.haml b/app/views/roles/index.html.haml index ae51b2d36..8548645c4 100644 --- a/app/views/roles/index.html.haml +++ b/app/views/roles/index.html.haml @@ -1,4 +1,4 @@ -%h1 Listing roles +- content_for :title, "Listing roles" - if can? :create, Role %p= link_to 'New Role', new_role_path, :class => 'btn btn-primary' diff --git a/app/views/roles/new.html.haml b/app/views/roles/new.html.haml index 6e07588dd..30aae3fe1 100644 --- a/app/views/roles/new.html.haml +++ b/app/views/roles/new.html.haml @@ -1,4 +1,4 @@ -%h1 New role +- content_for :title, "New role" = render 'form' From 496c070efd3d88415a3af9708880366193d5fe02 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 14:14:24 +0100 Subject: [PATCH 105/132] Tweaked layout of crop page sidebar --- app/views/crops/_find_seeds.html.haml | 4 +++- app/views/crops/_harvests.html.haml | 8 ++++---- app/views/crops/_plantings.html.haml | 8 ++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/app/views/crops/_find_seeds.html.haml b/app/views/crops/_find_seeds.html.haml index db7c5b4b7..9b7797144 100644 --- a/app/views/crops/_find_seeds.html.haml +++ b/app/views/crops/_find_seeds.html.haml @@ -8,7 +8,9 @@ %li = link_to "#{seed.owner} will trade #{seed.tradable_to}.", seed_path(seed) = render :partial => 'members/location', :locals => { :member => seed.owner } + %p + = link_to "View all #{crop.name} seeds", seeds_by_crop_path(crop) - if current_member - = link_to "List your seeds to trade.", new_seed_path() + = link_to "List achiote seeds to trade", new_seed_path(:crop_id => crop.id) - else = render :partial => 'shared/signin_signup', :locals => { :to => 'list your seeds to trade' } diff --git a/app/views/crops/_harvests.html.haml b/app/views/crops/_harvests.html.haml index d10f57d06..bac61e580 100644 --- a/app/views/crops/_harvests.html.haml +++ b/app/views/crops/_harvests.html.haml @@ -11,11 +11,11 @@ %small = distance_of_time_in_words(harvest.created_at, Time.zone.now) ago. - %p.col-md-offset-1 - = link_to "See all #{crop.name} harvests", harvests_by_crop_path(crop) + %p + = link_to "View all #{crop.name} harvests", harvests_by_crop_path(crop) - if current_member - %p.col-md-offset-1 - = link_to "Track your #{crop.name} harvests.", new_harvest_path(:crop_id => crop.id) + %p + = link_to "Harvest #{crop.name}", new_harvest_path(:crop_id => crop.id) - else = render :partial => 'shared/signin_signup', :locals => { :to => "track your #{crop.name} harvests" } diff --git a/app/views/crops/_plantings.html.haml b/app/views/crops/_plantings.html.haml index 72bce5608..85a2ff88a 100644 --- a/app/views/crops/_plantings.html.haml +++ b/app/views/crops/_plantings.html.haml @@ -11,11 +11,11 @@ %small = distance_of_time_in_words(planting.created_at, Time.zone.now) ago. - %p.col-md-offset-1 - = link_to "See all #{crop.name} plantings", plantings_by_crop_path(crop) + %p + = link_to "View all #{crop.name} plantings", plantings_by_crop_path(crop) - if current_member - %p.col-md-offset-1 - = link_to "Track your #{crop.name} plantings.", new_planting_path(:crop_id => crop.id) + %p + = link_to "Plant #{crop.name}", new_planting_path(:crop_id => crop.id) - else = render :partial => 'shared/signin_signup', :locals => { :to => "track your #{crop.name} plantings" } From dc1afb8913903760aada7f90a7f153222bac4c3e Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 14:25:47 +0100 Subject: [PATCH 106/132] Added seeds_by_crop_path routes and stuff --- app/controllers/plantings_controller.rb | 6 +++--- app/controllers/seeds_controller.rb | 3 +++ app/views/harvests/index.html.haml | 4 ++-- app/views/seeds/index.html.haml | 6 +++--- config/routes.rb | 1 + spec/controllers/member_controller_spec.rb | 8 ++++---- 6 files changed, 16 insertions(+), 12 deletions(-) diff --git a/app/controllers/plantings_controller.rb b/app/controllers/plantings_controller.rb index 62eaf5b79..b08fe98fd 100644 --- a/app/controllers/plantings_controller.rb +++ b/app/controllers/plantings_controller.rb @@ -9,11 +9,11 @@ class PlantingsController < ApplicationController @owner = Member.find_by_slug(params[:owner]) @crop = Crop.find_by_slug(params[:crop]) if @owner - @plantings = @owner.plantings.includes(:owner, :crop, :garden) + @plantings = @owner.plantings.includes(:owner, :crop, :garden).paginate(:page => params[:page]) elsif @crop - @plantings = @crop.plantings.includes(:owner, :crop, :garden) + @plantings = @crop.plantings.includes(:owner, :crop, :garden).paginate(:page => params[:page]) else - @plantings = Planting.includes(:owner, :crop, :garden) + @plantings = Planting.includes(:owner, :crop, :garden).paginate(:page => params[:page]) end respond_to do |format| diff --git a/app/controllers/seeds_controller.rb b/app/controllers/seeds_controller.rb index 30918b12e..03960624b 100644 --- a/app/controllers/seeds_controller.rb +++ b/app/controllers/seeds_controller.rb @@ -7,8 +7,11 @@ class SeedsController < ApplicationController # GET /seeds.json def index @owner = Member.find_by_slug(params[:owner]) + @crop = Crop.find_by_slug(params[:crop]) if @owner @seeds = @owner.seeds.includes(:owner, :crop).paginate(:page => params[:page]) + elsif @crop + @seeds = @crop.seeds.includes(:owner, :crop).paginate(:page => params[:page]) else @seeds = Seed.includes(:owner, :crop).paginate(:page => params[:page]) end diff --git a/app/views/harvests/index.html.haml b/app/views/harvests/index.html.haml index f80ce0dd5..f50129c9a 100644 --- a/app/views/harvests/index.html.haml +++ b/app/views/harvests/index.html.haml @@ -10,11 +10,11 @@ %p - if @owner == current_member = link_to 'Add harvest', new_harvest_path, :class => 'btn btn-primary' - = link_to "View everyone's harvests", harvests_path, :class => 'btn' + = link_to "View everyone's harvests", harvests_path, :class => 'btn btn-default' - else # everyone's harvests = link_to 'Add harvest', new_harvest_path, :class => 'btn btn-primary' - if current_member - = link_to 'View your harvests', harvests_by_owner_path(:owner => current_member.slug), :class => 'btn' + = link_to 'View your harvests', harvests_by_owner_path(:owner => current_member.slug), :class => 'btn btn-default' - else = render :partial => 'shared/signin_signup', :locals => { :to => 'track your harvests' } diff --git a/app/views/seeds/index.html.haml b/app/views/seeds/index.html.haml index faee35465..9993e83ec 100644 --- a/app/views/seeds/index.html.haml +++ b/app/views/seeds/index.html.haml @@ -1,4 +1,4 @@ -- content_for :title, @owner ? "#{@owner}'s seeds" : "Everyone's seeds" +- content_for :title, @owner ? "#{@owner}'s seeds" : @crop ? "Everyone's #{@crop.name} seeds" : "Everyone's seeds" %p #{ENV['GROWSTUFF_SITE_NAME']} helps you track your seed @@ -10,11 +10,11 @@ %p - if @owner == current_member = link_to 'Add seeds', new_seed_path, :class => 'btn btn-primary' - = link_to "View everyone's seeds", seeds_path, :class => 'btn' + = link_to "View everyone's seeds", seeds_path, :class => 'btn btn-default' - else # everyone's seeds = link_to 'Add seeds', new_seed_path, :class => 'btn btn-primary' - if current_member - = link_to 'View your seeds', seeds_by_owner_path(:owner => current_member.slug), :class => 'btn' + = link_to 'View your seeds', seeds_by_owner_path(:owner => current_member.slug), :class => 'btn btn-default' - else = render :partial => 'shared/signin_signup', :locals => { :to => 'add seeds to your stash' } diff --git a/config/routes.rb b/config/routes.rb index 0d645a1be..bdef427d4 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -19,6 +19,7 @@ Growstuff::Application.routes.draw do resources :seeds match '/seeds/owner/:owner' => 'seeds#index', :as => 'seeds_by_owner' + match '/seeds/crop/:crop' => 'seeds#index', :as => 'seeds_by_crop' resources :harvests match '/harvests/owner/:owner' => 'harvests#index', :as => 'harvests_by_owner' diff --git a/spec/controllers/member_controller_spec.rb b/spec/controllers/member_controller_spec.rb index 906a12b82..ff5a0ae42 100644 --- a/spec/controllers/member_controller_spec.rb +++ b/spec/controllers/member_controller_spec.rb @@ -17,17 +17,17 @@ describe MembersController do end describe "GET JSON index" do - it "does NOT provide JSON for members" do + it "provides JSON for members" do get :index, :format => 'json' - response.should_not be_success + response.should be_success end end describe "GET show" do - it "does NOT provide JSON for member profile" do + it "provides JSON for member profile" do get :show, { :id => @member.id , :format => 'json' } - response.should_not be_success + response.should be_success end it "assigns @posts with the member's posts" do From 1daee9612c9d1f00d0eccf918c125164c7e2c30f Mon Sep 17 00:00:00 2001 From: emmawinston Date: Sun, 19 Oct 2014 14:52:10 +0100 Subject: [PATCH 107/132] Changed set to album on photo page --- app/views/photos/new.html.haml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/views/photos/new.html.haml b/app/views/photos/new.html.haml index bc3eb35df..7b89997be 100644 --- a/app/views/photos/new.html.haml +++ b/app/views/photos/new.html.haml @@ -13,7 +13,7 @@ - if @sets and @sets.length > 0 %p = form_tag(new_photo_path, :method => :get, :class => 'form-inline') do - = label_tag :set, "Choose a photo set:", :class => 'control-label' + = label_tag :set, "Choose a photo album:", :class => 'control-label' = select_tag :set, options_for_select(@sets, @current_set), :class => 'input-large' = hidden_field_tag :type, @type = hidden_field_tag :id, @id @@ -36,4 +36,3 @@ You must =link_to "connect your account to Flickr", '/auth/flickr' to add photos. - From e5cd14cd4539a884e2d862f66baff7e2eb51b315 Mon Sep 17 00:00:00 2001 From: emmawinston Date: Sun, 19 Oct 2014 14:56:44 +0100 Subject: [PATCH 108/132] Added myself to the contributors file --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index cdec8a25a..650f8c321 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -47,3 +47,4 @@ submit the change with your pull request. - Cheri Allen / [cherimarie](https://github.com/cherimarie) - Maki Sugita / [macckii](https:://github.com/macckii) - Shiho Takagi / [oshiho3](https://github.com/oshiho3) +- Emma Winston / [emmawinston](https://github.com/emmawinston) From ef8d55230157516674d1f34592b1be7f66616c19 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 15:06:34 +0100 Subject: [PATCH 109/132] Update Ruby to 2.1.2 in .travis.yml --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index f65de9cb5..7ed73fce0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,7 +4,7 @@ language: ruby env: GROWSTUFF_SITE_NAME="Growstuff (travis)" RAILS_SECRET_TOKEN='xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' bundler_args: --without development production staging rvm: - - 2.1.1 + - 2.1.2 before_script: - psql -c 'create database growstuff_test;' -U postgres script: From 2aabcce70d9cfcb734410ebd8783308ead4959f6 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 12:13:17 +0100 Subject: [PATCH 110/132] Feature tests for scientific names. --- spec/features/scientific_name_spec.rb | 66 +++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) create mode 100644 spec/features/scientific_name_spec.rb diff --git a/spec/features/scientific_name_spec.rb b/spec/features/scientific_name_spec.rb new file mode 100644 index 000000000..5a3a82989 --- /dev/null +++ b/spec/features/scientific_name_spec.rb @@ -0,0 +1,66 @@ +require 'spec_helper' + +feature "Scientific names" do + let!(:zea_mays) { FactoryGirl.create(:zea_mays) } + let(:crop) { zea_mays.crop } + + scenario "Display scientific names on crop page" do + visit crop_path(zea_mays.crop) + expect(page).to have_content zea_mays.scientific_name + end + + scenario "Index page for scientific names" do + visit scientific_names_path + expect(page).to have_content zea_mays.scientific_name + end + + context "User is a crop wrangler" do + let!(:crop_wranglers) { FactoryGirl.create_list(:crop_wrangling_member, 3) } + let(:member){crop_wranglers.first} + + before :each do + login_as(member) + end + + scenario "Crop wranglers can edit scientific names" do + visit crop_path(crop) + expect(page).to have_content "CROP WRANGLER" + expect(page).to have_content zea_mays.scientific_name + expect(page).to have_link "Edit", :href => edit_scientific_name_path(zea_mays) + within('.scientific_names') { click_on "Edit" } + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Scientific name', with: "Zea mirabila" + click_on "Save" + expect(page).to have_content "Zea mirabila" + expect(page).to have_content 'Scientific name was successfully updated' + end + + scenario "Crop wranglers can delete scientific names" do + visit crop_path(zea_mays.crop) + expect(page).to have_link "Delete", + href: scientific_name_path(zea_mays) + within('.scientific_names') { click_on "Delete" } + expect(page).to_not have_content zea_mays.scientific_name + expect(page).to have_content 'Scientific name was successfully deleted' + end + + scenario "Crop wranglers can add scientific names" do + visit crop_path(crop) + expect(page).to have_link "Add", + href: new_scientific_name_path(crop_id: crop.id) + within('.scientific_names') { click_on "Add" } + expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" + fill_in 'Scientific name', with: "Zea mirabila" + click_on "Save" + expect(page).to have_content "Zea mirabila" + expect(page).to have_content 'Scientific name was successfully created' + end + + scenario "The show-scientific-name page works" do + visit scientific_name_path(zea_mays) + expect(page).to have_content zea_mays.crop.name + end + + end + +end From 6de2112c4d042e95d0555d9b0ac8018ac6606cb2 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 12:16:05 +0100 Subject: [PATCH 111/132] Display note on successful sciname deletion. --- app/controllers/scientific_names_controller.rb | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/controllers/scientific_names_controller.rb b/app/controllers/scientific_names_controller.rb index 202c3942c..3911a6618 100644 --- a/app/controllers/scientific_names_controller.rb +++ b/app/controllers/scientific_names_controller.rb @@ -83,7 +83,9 @@ class ScientificNamesController < ApplicationController @scientific_name.destroy respond_to do |format| - format.html { redirect_to @crop } + format.html { + redirect_to @crop, notice: 'Scientific name was successfully deleted.' + } format.json { head :no_content } end end From 56b7d89d9ea709033ae5d74eba182a0c694240bb Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 12:19:55 +0100 Subject: [PATCH 112/132] Link scinames to crops, don't show numeric crop IDs --- app/views/scientific_names/show.html.haml | 2 +- spec/features/scientific_name_spec.rb | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/scientific_names/show.html.haml b/app/views/scientific_names/show.html.haml index 23e3a3c47..a3efe2d6f 100644 --- a/app/views/scientific_names/show.html.haml +++ b/app/views/scientific_names/show.html.haml @@ -5,7 +5,7 @@ = @scientific_name.scientific_name %p %b Crop: - = @scientific_name.crop_id + = link_to @scientific_name.crop, @scientific_name.crop = link_to 'Edit', edit_scientific_name_path(@scientific_name), :class => 'btn btn-default btn-xs' \| diff --git a/spec/features/scientific_name_spec.rb b/spec/features/scientific_name_spec.rb index 5a3a82989..01093a9c2 100644 --- a/spec/features/scientific_name_spec.rb +++ b/spec/features/scientific_name_spec.rb @@ -58,7 +58,8 @@ feature "Scientific names" do scenario "The show-scientific-name page works" do visit scientific_name_path(zea_mays) - expect(page).to have_content zea_mays.crop.name + expect(page).to have_link zea_mays.crop.name, + href: crop_path(zea_mays.crop) end end From b17aaf262dc04b916e46269d32362ba4ac74683a Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 19 Oct 2014 12:25:41 +0100 Subject: [PATCH 113/132] Test HTTP statuses in sciname feature tests This makes it easier to tell the difference between "test failed because the expected content isn't there" and "test failed because the whole page is broken". It also guards against "test passed incorrectly because the expected content was part of the error message". --- spec/features/scientific_name_spec.rb | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/spec/features/scientific_name_spec.rb b/spec/features/scientific_name_spec.rb index 01093a9c2..1fda79d42 100644 --- a/spec/features/scientific_name_spec.rb +++ b/spec/features/scientific_name_spec.rb @@ -6,11 +6,13 @@ feature "Scientific names" do scenario "Display scientific names on crop page" do visit crop_path(zea_mays.crop) + expect(page.status_code).to equal 200 expect(page).to have_content zea_mays.scientific_name end scenario "Index page for scientific names" do visit scientific_names_path + expect(page.status_code).to equal 200 expect(page).to have_content zea_mays.scientific_name end @@ -24,10 +26,12 @@ feature "Scientific names" do scenario "Crop wranglers can edit scientific names" do visit crop_path(crop) + expect(page.status_code).to equal 200 expect(page).to have_content "CROP WRANGLER" expect(page).to have_content zea_mays.scientific_name expect(page).to have_link "Edit", :href => edit_scientific_name_path(zea_mays) within('.scientific_names') { click_on "Edit" } + expect(page.status_code).to equal 200 expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" fill_in 'Scientific name', with: "Zea mirabila" click_on "Save" @@ -40,6 +44,7 @@ feature "Scientific names" do expect(page).to have_link "Delete", href: scientific_name_path(zea_mays) within('.scientific_names') { click_on "Delete" } + expect(page.status_code).to equal 200 expect(page).to_not have_content zea_mays.scientific_name expect(page).to have_content 'Scientific name was successfully deleted' end @@ -49,15 +54,18 @@ feature "Scientific names" do expect(page).to have_link "Add", href: new_scientific_name_path(crop_id: crop.id) within('.scientific_names') { click_on "Add" } + expect(page.status_code).to equal 200 expect(page).to have_css "option[value='#{crop.id}'][selected=selected]" fill_in 'Scientific name', with: "Zea mirabila" click_on "Save" + expect(page.status_code).to equal 200 expect(page).to have_content "Zea mirabila" expect(page).to have_content 'Scientific name was successfully created' end scenario "The show-scientific-name page works" do visit scientific_name_path(zea_mays) + expect(page.status_code).to equal 200 expect(page).to have_link zea_mays.crop.name, href: crop_path(zea_mays.crop) end From 58f1bca68362148bf15abec10c6a1b6062b5c198 Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 16:51:08 +0100 Subject: [PATCH 114/132] removed spurious console.log calls --- app/assets/javascripts/members.js.erb | 2 -- 1 file changed, 2 deletions(-) diff --git a/app/assets/javascripts/members.js.erb b/app/assets/javascripts/members.js.erb index a3f29904d..f55f35732 100644 --- a/app/assets/javascripts/members.js.erb +++ b/app/assets/javascripts/members.js.erb @@ -6,7 +6,6 @@ if (document.getElementById("membermap") !== null) { $.getJSON(location.pathname + '.json', function(member) { - console.log(JSON.stringify(member.latitude)); if (member.latitude && member.longitude) { membermap = L.map('membermap').setView([member.latitude, member.longitude], 4); @@ -14,7 +13,6 @@ if (document.getElementById("membermap") !== null) { attribution: 'Map data © OpenStreetMap contributors under ODbL | Map imagery © Mapbox', maxZoom: 18 }).addTo(membermap); - console.log("found lat and long") marker = new L.Marker(new L.LatLng(member.latitude, member.longitude)); member_url = "/members/" + member.slug; From a25eb2d631ef1a8319237580f0c8bf4879e180de Mon Sep 17 00:00:00 2001 From: Skud Date: Sun, 19 Oct 2014 16:52:45 +0100 Subject: [PATCH 115/132] Deleted old view tests for crops --- spec/views/crops/show.html.haml_spec.rb | 242 ------------------------ 1 file changed, 242 deletions(-) delete mode 100644 spec/views/crops/show.html.haml_spec.rb diff --git a/spec/views/crops/show.html.haml_spec.rb b/spec/views/crops/show.html.haml_spec.rb deleted file mode 100644 index ca6166721..000000000 --- a/spec/views/crops/show.html.haml_spec.rb +++ /dev/null @@ -1,242 +0,0 @@ -require 'spec_helper' - -describe "crops/show" do - before(:each) do - controller.stub(:current_user) { nil } - @crop = FactoryGirl.create(:maize, - :scientific_names => [ FactoryGirl.create(:zea_mays) ] - ) - assign(:crop, @crop) - @author = FactoryGirl.create(:member) - page = 1 - per_page = 2 - total_entries = 2 - @posts = WillPaginate::Collection.create(page, per_page, total_entries) do |pager| - pager.replace([ - @post1 = FactoryGirl.create(:post, :author => @author, :body => "Post it!" ), - @post2 = FactoryGirl.create(:post, :author => @author, :body => "Done!" ) - ]) - end - end - - context 'photos' do - before(:each) do - @planting = FactoryGirl.create(:planting, :crop => @crop) - @photo1 = FactoryGirl.create(:photo) - @photo2 = FactoryGirl.create(:photo) - @photo3 = FactoryGirl.create(:photo) - @planting.photos << [@photo1, @photo2, @photo3] - render - end - - it 'shows 3 photos across the top of the page' do - assert_select "div.thumbnail>a>img", :count => 3 - end - - it 'links to the photo detail page' do - assert_select "a[href=#{photo_path(@photo1)}]" - end - - it 'links to the photo owner' do - assert_select "a[href=#{member_path(@photo1.owner)}]" - end - end - - context "map" do - it "has a map" do - render - assert_select("div#cropmap") - end - - it "explains what's shown on the map" do - render - rendered.should contain "Only plantings by members who have set their locations are shown on this map" - end - - it "shows a 'set your location' link to people who need to" do - @nowhere = FactoryGirl.create(:member) - sign_in @nowhere - controller.stub(:current_user) { @nowhere } - render - rendered.should contain "Set your location" - end - - it "doesn't show 'set your location' to people who have one" do - @somewhere = FactoryGirl.create(:london_member) - sign_in @somewhere - controller.stub(:current_user) { @somewhere } - render - rendered.should_not contain "Set your location" - end - - end - - it "shows the wikipedia URL" do - render - assert_select("a[href=#{@crop.en_wikipedia_url}]", 'Wikipedia (English)') - end - - it "shows the scientific name" do - render - rendered.should contain "Scientific names" - rendered.should contain "Zea mays" - end - - context "seeds available for trade" do - before(:each) do - @owner1 = FactoryGirl.create(:london_member) - @owner2 = FactoryGirl.create(:member) # no location - @seed1 = FactoryGirl.create(:tradable_seed, :owner => @owner1, :crop => @crop) - @seed2 = FactoryGirl.create(:tradable_seed, :owner => @owner2, :crop => @crop) - render - end - - it "shows a heading" do - rendered.should contain "Find seeds" - end - - it "shows a list of people with seeds to trade" do - @crop.seeds.each do |seed| - assert_select "a[href=#{seed_path(seed)}]" - end - end - end - - context "harvests" do - before(:each) do - @owner1 = FactoryGirl.create(:london_member) - @h1 = FactoryGirl.create(:harvest, :owner => @owner1, :crop => @crop) - @h2 = FactoryGirl.create(:harvest, :owner => @owner1, :crop => @crop) - render - end - - it "shows a heading" do - rendered.should contain "Harvests" - end - - it "shows a list of people who have harvested this crop" do - @crop.harvests.each do |harvest| - assert_select "a[href=#{harvest_path(harvest)}]" - end - end - end - - context "no seeds available for trade" do - it "shows a heading" do - render - rendered.should contain "Find seeds" - end - - it "suggests you trade seeds" do - render - rendered.should contain "There are no seeds available to trade." - end - end - - context "has plantings" do - before(:each) do - @owner = FactoryGirl.create(:london_member) - @planting = FactoryGirl.create(:planting, - :crop => @crop, - :owner => @owner - ) - @crop.reload # to pick up latest plantings_count - end - - it "links to people who are growing this crop" do - render - rendered.should contain @owner.login_name - rendered.should contain @owner.location - end - end - - context "has posts" do - it "links to posts" do - render - @posts.each do |p| - rendered.should contain p.author.login_name - rendered.should contain p.subject - rendered.should contain p.body - end - end - - it "contains two gravatar icons" do - render - assert_select "img", :src => /gravatar\.com\/avatar/, :count => 2 - end - end - - context 'varieties' do - before(:each) do - @popcorn = FactoryGirl.create(:popcorn, :parent_id => @crop.id) - @ubercrop = FactoryGirl.create(:crop, :name => 'ubercrop') - @crop.parent_id = @ubercrop.id - @crop.save - render - end - - it 'shows popcorn as a child variety' do - rendered.should contain @popcorn.name - end - - it 'shows parent crop' do - rendered.should contain @ubercrop.name - end - - end - - context 'logged in' do - before(:each) do - @member = FactoryGirl.create(:member) - sign_in @member - controller.stub(:current_user) { @member } - render - end - - it { rendered.should contain "Nobody has planted this crop yet" } - it { rendered.should contain "Nobody has harvested this crop yet" } - - context "should have a link to" do - before do - FactoryGirl.create(:planting, :crop => @crop) - FactoryGirl.create(:harvest, :crop => @crop) - @crop.reload - render - end - - it "show all plantings by the crop link" do - assert_select("a[href=#{plantings_by_crop_path @crop}]") - end - - it "show all harvests by the crop link" do - assert_select("a[href=#{harvests_by_crop_path @crop}]") - end - end - - - - end - - context "logged in and crop wrangler" do - - before(:each) do - @member = FactoryGirl.create(:crop_wrangling_member) - sign_in @member - controller.stub(:current_user) { @member } - render - end - - it "links to the edit crop form" do - assert_select "a[href=#{edit_crop_path(@crop)}]", :text => "Edit crop" - end - - it "links to the add scientific name form" do - assert_select "a[href^=#{new_scientific_name_path}]", :text => "Add" - end - - it "links to the edit scientific name form" do - assert_select "a[href=#{edit_scientific_name_path(@crop.scientific_names.first)}]", :text => "Edit" - end - end - -end From ab449a65f8ca261abc6b16d10986d4939eddec93 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Mon, 20 Oct 2014 21:27:39 +1100 Subject: [PATCH 116/132] remove 'to be translated' message from ja locale file --- config/locales/ja.yml | 75 +------------------------------------------ 1 file changed, 1 insertion(+), 74 deletions(-) diff --git a/config/locales/ja.yml b/config/locales/ja.yml index 1f2fe3247..925a54399 100644 --- a/config/locales/ja.yml +++ b/config/locales/ja.yml @@ -2,77 +2,4 @@ ja: home: blurb: intro: "%{site_name}はガーデナーのコミュニティです。" - perks: "翻訳中" - sign_up: "翻訳中" - already_html: "翻訳中" - sign_in_linktext: "翻訳中" - - crops: - our_crops: "翻訳中" - recently_planted: "翻訳中" - recently_added: "翻訳中" - view_all: "翻訳中" - - discuss: - discussion: "翻訳中" - forums: "翻訳中" - view_all: "翻訳中" - - keep_in_touch: - keep_in_touch: "翻訳中" - twitter_html: "翻訳中" - twitter_linktext: "翻訳中" - blog_html: "翻訳中" - blog_linktext: "翻訳中" - newsletter_html: "翻訳中" - newsletter_linktext: "翻訳中" - - members: - title: "翻訳中" - view_all: "翻訳中" - - open: - open_source_title: "翻訳中" - open_source_body_html: "翻訳中" - why_linktext: "翻訳中" - github_linktext: "翻訳中" - open_data_title: "翻訳中" - open_data_body_html: "翻訳中" - creative_commons_linktext: "翻訳中" - wiki_linktext: "翻訳中" - api_docs_linktext: "翻訳中" - get_involved_title: "翻訳中" - get_involved_body_html: "翻訳中" - talk_linktext: "翻訳中" - wiki_linktext: "翻訳中" - support_title: "翻訳中" - support_body_html: "翻訳中" - ad_free_linktext: "翻訳中" - - - seeds: - title: "翻訳中" - owner: "翻訳中" - crop: "翻訳中" - description: "翻訳中" - trade_to: "翻訳中" - from: "翻訳中" - unspecified: "翻訳中" - details: "翻訳中" - view_all: "翻訳中" - - stats: - message_html: "翻訳中" - member_linktext: "翻訳中" - number_crops_linktext: "翻訳中" - number_plantings_linktext: "翻訳中" - number_gardens_linktext: "翻訳中" - - index: - welcome: "翻訳中" - plant: "翻訳中" - harvest: "翻訳中" - add_seeds: "翻訳中" - post: "翻訳中" - edit_profile: "翻訳中" - + \ No newline at end of file From a6508b5c9fe804e80127ab024ea9f11c109fcab8 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Mon, 20 Oct 2014 22:47:13 +1100 Subject: [PATCH 117/132] replace debugger with byebug --- Gemfile | 4 +--- Gemfile.lock | 11 +++++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index 2360041eb..310292a25 100644 --- a/Gemfile +++ b/Gemfile @@ -80,9 +80,6 @@ gem 'flickraw' # To use debugger group :development do - # Installation of the debugger gem fails on Travis CI, - # so we don't use it in the test environment - gem 'debugger' # A debugger and irb alternative. Pry doesn't play nice # with unicorn, so start a Webrick server when debugging # with Pry @@ -123,6 +120,7 @@ gem 'omniauth-flickr', '>= 0.0.15' gem 'rake', '>= 10.0.0' group :development, :test do + gem 'byebug' # debugging gem 'haml-rails' # HTML templating language gem 'rspec-rails', '~> 2.12.1' # unit testing framework gem 'database_cleaner', '~> 1.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index fae16d6ad..cb7cb2688 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,10 @@ GEM bootstrap-datepicker-rails (1.3.0.2) railties (>= 3.0) builder (3.0.4) + byebug (3.5.1) + columnize (~> 0.8) + debugger-linecache (~> 1.2) + slop (~> 3.6) cancan (1.6.10) capybara (2.4.1) mime-types (>= 1.16) @@ -96,12 +100,7 @@ GEM dalli (2.7.2) database_cleaner (1.3.0) debug_inspector (0.0.2) - debugger (1.6.8) - columnize (>= 0.3.1) - debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.3.5) debugger-linecache (1.2.0) - debugger-ruby_core_source (1.3.5) devise (3.2.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -328,6 +327,7 @@ DEPENDENCIES bluecloth bootstrap-datepicker-rails bundler (>= 1.1.5) + byebug cancan capybara coffee-rails (~> 3.2.1) @@ -336,7 +336,6 @@ DEPENDENCIES csv_shaper dalli database_cleaner (~> 1.3.0) - debugger devise (~> 3.2.0) factory_girl_rails (~> 4.0) figaro From 48875dacb3bcb8440b93b9f9c42f1c80c5921f02 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Tue, 21 Oct 2014 06:53:21 +1100 Subject: [PATCH 118/132] remove pry because it's redundant with byebug --- Gemfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Gemfile b/Gemfile index 310292a25..1739744bf 100644 --- a/Gemfile +++ b/Gemfile @@ -80,10 +80,6 @@ gem 'flickraw' # To use debugger group :development do - # A debugger and irb alternative. Pry doesn't play nice - # with unicorn, so start a Webrick server when debugging - # with Pry - gem 'pry' gem 'better_errors' gem 'binding_of_caller' gem 'letter_opener' From c244da8b73c98945d49340ede84946bfbcbf291b Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Tue, 21 Oct 2014 07:24:08 +1100 Subject: [PATCH 119/132] make English fallback language --- Gemfile | 3 --- Gemfile.lock | 8 -------- config/application.rb | 8 ++++++-- config/initializers/locale.rb | 4 ---- 4 files changed, 6 insertions(+), 17 deletions(-) delete mode 100644 config/initializers/locale.rb diff --git a/Gemfile b/Gemfile index 2360041eb..124c1f4f9 100644 --- a/Gemfile +++ b/Gemfile @@ -80,9 +80,6 @@ gem 'flickraw' # To use debugger group :development do - # Installation of the debugger gem fails on Travis CI, - # so we don't use it in the test environment - gem 'debugger' # A debugger and irb alternative. Pry doesn't play nice # with unicorn, so start a Webrick server when debugging # with Pry diff --git a/Gemfile.lock b/Gemfile.lock index fae16d6ad..6f6b7d1a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,7 +77,6 @@ GEM coffee-script-source execjs coffee-script-source (1.8.0) - columnize (0.8.9) commonjs (0.2.7) compass (0.12.7) chunky_png (~> 1.2) @@ -96,12 +95,6 @@ GEM dalli (2.7.2) database_cleaner (1.3.0) debug_inspector (0.0.2) - debugger (1.6.8) - columnize (>= 0.3.1) - debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.3.5) - debugger-linecache (1.2.0) - debugger-ruby_core_source (1.3.5) devise (3.2.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -336,7 +329,6 @@ DEPENDENCIES csv_shaper dalli database_cleaner (~> 1.3.0) - debugger devise (~> 3.2.0) factory_girl_rails (~> 4.0) figaro diff --git a/config/application.rb b/config/application.rb index 6bbe1fe7d..5b01d3aa8 100644 --- a/config/application.rb +++ b/config/application.rb @@ -31,8 +31,12 @@ module Growstuff config.active_record.default_timezone = :local # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded. - # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s] - # config.i18n.default_locale = :de + I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] + I18n.default_locale = :en + # rails will fallback to config.i18n.default_locale translation + config.i18n.fallbacks = true + # rails will fallback to en, no matter what is set as config.i18n.default_locale + config.i18n.fallbacks = [:en] # Configure the default encoding used in templates for Ruby 1.9. config.encoding = "utf-8" diff --git a/config/initializers/locale.rb b/config/initializers/locale.rb deleted file mode 100644 index fb251b800..000000000 --- a/config/initializers/locale.rb +++ /dev/null @@ -1,4 +0,0 @@ -I18n.load_path += Dir[Rails.root.join('config', 'locales', '*.{rb,yml}')] -I18n.default_locale = :en - - \ No newline at end of file From e9d1d11535b08d4baca9c4266d4ee3345fb668d3 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Tue, 21 Oct 2014 19:43:51 +1100 Subject: [PATCH 120/132] add append date functionality for planting finished on garden show page and planting list page --- Gemfile | 4 ---- Gemfile.lock | 8 -------- app/views/plantings/_thumbnail.html.haml | 2 +- app/views/plantings/index.html.haml | 2 +- 4 files changed, 2 insertions(+), 14 deletions(-) diff --git a/Gemfile b/Gemfile index 2360041eb..65000a6d0 100644 --- a/Gemfile +++ b/Gemfile @@ -78,11 +78,7 @@ gem 'flickraw' # Use unicorn as the app server # gem 'unicorn' -# To use debugger group :development do - # Installation of the debugger gem fails on Travis CI, - # so we don't use it in the test environment - gem 'debugger' # A debugger and irb alternative. Pry doesn't play nice # with unicorn, so start a Webrick server when debugging # with Pry diff --git a/Gemfile.lock b/Gemfile.lock index fae16d6ad..6f6b7d1a5 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -77,7 +77,6 @@ GEM coffee-script-source execjs coffee-script-source (1.8.0) - columnize (0.8.9) commonjs (0.2.7) compass (0.12.7) chunky_png (~> 1.2) @@ -96,12 +95,6 @@ GEM dalli (2.7.2) database_cleaner (1.3.0) debug_inspector (0.0.2) - debugger (1.6.8) - columnize (>= 0.3.1) - debugger-linecache (~> 1.2.0) - debugger-ruby_core_source (~> 1.3.5) - debugger-linecache (1.2.0) - debugger-ruby_core_source (1.3.5) devise (3.2.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -336,7 +329,6 @@ DEPENDENCIES csv_shaper dalli database_cleaner (~> 1.3.0) - debugger devise (~> 3.2.0) factory_girl_rails (~> 4.0) figaro diff --git a/app/views/plantings/_thumbnail.html.haml b/app/views/plantings/_thumbnail.html.haml index c8588abf5..4d2b6cdee 100644 --- a/app/views/plantings/_thumbnail.html.haml +++ b/app/views/plantings/_thumbnail.html.haml @@ -42,6 +42,6 @@ - if can? :edit, planting =link_to 'Edit', edit_planting_path(planting), :class => 'btn btn-default btn-xs' - if ! planting.finished - = link_to "Mark as finished", planting_path(planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs' + = link_to "Mark as finished", planting_path(planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs append-date' - if can? :destroy, planting =link_to 'Delete', planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index eb1e017dc..e3edabae5 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -53,7 +53,7 @@ - if can? :edit, planting =link_to 'Edit', edit_planting_path(planting), :class => 'btn btn-default btn-xs' - if ! planting.finished - = link_to "Mark as finished", planting_path(planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs' + = link_to "Mark as finished", planting_path(planting, :planting => {:finished => 1}), :method => :put, :class => 'btn btn-default btn-xs append-date' - if can? :destroy, planting =link_to 'Delete', planting, method: :delete, data: { confirm: 'Are you sure?' }, :class => 'btn btn-default btn-xs' From 9b10a7c3ceb0d9cb1f8b368342ceb14ac6e36cbb Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 23 Oct 2014 22:18:52 +1100 Subject: [PATCH 121/132] write shared example group for append date --- Gemfile | 1 + Gemfile.lock | 7 ++++++ spec/features/gardens_spec.rb | 9 +++++++ .../plantings/planting_a_crop_spec.rb | 24 +++++++++---------- .../shared_examples/append_date_spec.rb | 15 ++++++++++++ .../shared_examples/crop_suggest_spec.rb | 2 -- 6 files changed, 44 insertions(+), 14 deletions(-) create mode 100644 spec/features/shared_examples/append_date_spec.rb diff --git a/Gemfile b/Gemfile index 65000a6d0..939a146b2 100644 --- a/Gemfile +++ b/Gemfile @@ -119,6 +119,7 @@ gem 'omniauth-flickr', '>= 0.0.15' gem 'rake', '>= 10.0.0' group :development, :test do + gem 'byebug' gem 'haml-rails' # HTML templating language gem 'rspec-rails', '~> 2.12.1' # unit testing framework gem 'database_cleaner', '~> 1.3.0' diff --git a/Gemfile.lock b/Gemfile.lock index 6f6b7d1a5..cb7cb2688 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -60,6 +60,10 @@ GEM bootstrap-datepicker-rails (1.3.0.2) railties (>= 3.0) builder (3.0.4) + byebug (3.5.1) + columnize (~> 0.8) + debugger-linecache (~> 1.2) + slop (~> 3.6) cancan (1.6.10) capybara (2.4.1) mime-types (>= 1.16) @@ -77,6 +81,7 @@ GEM coffee-script-source execjs coffee-script-source (1.8.0) + columnize (0.8.9) commonjs (0.2.7) compass (0.12.7) chunky_png (~> 1.2) @@ -95,6 +100,7 @@ GEM dalli (2.7.2) database_cleaner (1.3.0) debug_inspector (0.0.2) + debugger-linecache (1.2.0) devise (3.2.4) bcrypt (~> 3.0) orm_adapter (~> 0.1) @@ -321,6 +327,7 @@ DEPENDENCIES bluecloth bootstrap-datepicker-rails bundler (>= 1.1.5) + byebug cancan capybara coffee-rails (~> 3.2.1) diff --git a/spec/features/gardens_spec.rb b/spec/features/gardens_spec.rb index 1b803a41e..2b4c22d8b 100644 --- a/spec/features/gardens_spec.rb +++ b/spec/features/gardens_spec.rb @@ -2,6 +2,8 @@ require 'spec_helper' feature "Planting a crop", :js => true do let!(:garden) { FactoryGirl.create(:garden) } + let!(:planting) { FactoryGirl.create(:planting, garden: garden, planted_at: Date.parse("2013-3-10")) } + background do login_as(garden.owner) @@ -54,4 +56,11 @@ feature "Planting a crop", :js => true do expect(page).to have_content "#{garden.owner}'s gardens" end + describe "Making a planting inactive from garden show" do + let(:path) { garden_path(garden) } + let(:link_text) { "Mark as finished" } + + it_behaves_like "append date" + end + end diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index d8fae5c64..3f903887a 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -4,14 +4,14 @@ feature "Planting a crop", :js => true do let(:member) { FactoryGirl.create(:member) } let!(:maize) { FactoryGirl.create(:maize) } let(:garden) { FactoryGirl.create(:garden, owner: member) } - let(:planting) { FactoryGirl.create(:planting, garden: garden, planted_at: Date.parse("2013-3-10")) } + let!(:planting) { FactoryGirl.create(:planting, garden: garden, planted_at: Date.parse("2013-3-10")) } background do login_as(member) visit "/plantings/new" end - it_behaves_like "crop suggest", "planting", "crop" + it_behaves_like "crop suggest", "planting" scenario "Creating a new planting" do fill_autocomplete "crop", :with => "m" @@ -84,16 +84,16 @@ feature "Planting a crop", :js => true do expect(page).to have_content "Finished: Yes (no date specified)" end - scenario "Marking a planting as finished from the show page" do - this_month = Date.today.strftime("%B") - this_year = Date.today.strftime("%Y") - visit planting_path(planting) - click_link "Mark as finished" - within "div.datepicker" do - expect(page).to have_content "#{this_month}" - page.find(".datepicker-days td.day", text: "21").click - end - expect(page).to have_content "Finished: #{this_month} 21, #{this_year}" + describe "Marking a planting as finished from the show page" do + let(:path) { planting_path(planting) } + let(:link_text) { "Mark as finished" } + it_behaves_like "append date" + end + + describe "Marking a planting as finished from the list page" do + let(:path) { plantings_path } + let(:link_text) { "Mark as finished" } + it_behaves_like "append date" end end diff --git a/spec/features/shared_examples/append_date_spec.rb b/spec/features/shared_examples/append_date_spec.rb new file mode 100644 index 000000000..bf4a297b1 --- /dev/null +++ b/spec/features/shared_examples/append_date_spec.rb @@ -0,0 +1,15 @@ +shared_examples "append date" do + + scenario "Displaying a datepicker" do + this_month = Date.today.strftime("%B") + this_year = Date.today.strftime("%Y") + visit path + click_link link_text + within "div.datepicker" do + expect(page).to have_content "#{this_month}" + page.find(".datepicker-days td.day", text: "21").click + end + expect(page).to have_content "Finished: #{this_month} 21, #{this_year}" + end + +end \ No newline at end of file diff --git a/spec/features/shared_examples/crop_suggest_spec.rb b/spec/features/shared_examples/crop_suggest_spec.rb index 499bab891..da10072a3 100644 --- a/spec/features/shared_examples/crop_suggest_spec.rb +++ b/spec/features/shared_examples/crop_suggest_spec.rb @@ -1,5 +1,3 @@ -require 'spec_helper' - shared_examples "crop suggest" do |resource| let!(:popcorn) { FactoryGirl.create(:popcorn) } let!(:pear) { FactoryGirl.create(:pear) } From 0ccd6e843a1b1e79de69e89ee00aab965b51423a Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Thu, 23 Oct 2014 22:43:38 +1100 Subject: [PATCH 122/132] implement confirm without date functionality on appty on append date js --- app/assets/javascripts/append_date.js.coffee | 18 +++++++++++++++++- .../shared_examples/append_date_spec.rb | 9 ++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/append_date.js.coffee b/app/assets/javascripts/append_date.js.coffee index cf54f4654..8fc31de9d 100644 --- a/app/assets/javascripts/append_date.js.coffee +++ b/app/assets/javascripts/append_date.js.coffee @@ -8,12 +8,28 @@ jQuery -> el.datepicker({'format': 'yyyy-mm-dd'}) + href = el.attr('href') + + originalText = el.text() + el.click (e) -> e.stopPropagation() e.preventDefault() + $(this).text('Confirm without date') + + $(this).bind('click.confirm', (e) -> + link = $("") + $('body').append(link) + $(link).click() + ) + + $(this).blur (e) -> + $(this).text(originalText) + $(this).unbind('click.confirm') + + el.one 'changeDate', -> - href = $(this).attr('href') date = $(this).datepicker('getDate') url = "#{href}&planting[finished_at]=#{date}" diff --git a/spec/features/shared_examples/append_date_spec.rb b/spec/features/shared_examples/append_date_spec.rb index bf4a297b1..089680ff9 100644 --- a/spec/features/shared_examples/append_date_spec.rb +++ b/spec/features/shared_examples/append_date_spec.rb @@ -1,6 +1,6 @@ shared_examples "append date" do - scenario "Displaying a datepicker" do + scenario "Selecting a date with datepicker" do this_month = Date.today.strftime("%B") this_year = Date.today.strftime("%Y") visit path @@ -12,4 +12,11 @@ shared_examples "append date" do expect(page).to have_content "Finished: #{this_month} 21, #{this_year}" end + scenario "Confirming without selecting date" do + visit path + click_link link_text + click_link "Confirm without date" + expect(page).to have_content("Finished: Yes (no date specified) ") + end + end \ No newline at end of file From 6921119301a71389c6d5cbfcd5e847e440a0f449 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 2 Nov 2014 22:21:32 +0000 Subject: [PATCH 123/132] Make Capybara actually hit the server. Background: http://talk.growstuff.org/t/mysteriously-broken-feature-tests/120 @oshiho3's suggested fix didn't work, so I tried all the suggestions at http://stackoverflow.com/questions/6536503/capybara-with-subdomains-default-host until I found one that fixed the problem. --- config/environments/test.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/environments/test.rb b/config/environments/test.rb index 6f49b5ef0..6fb8cad3e 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -86,3 +86,7 @@ Geocoder::Lookup::Test.add_stub( # Unknown location Geocoder::Lookup::Test.add_stub( "Tatooine", []) + +Capybara.configure do |config| + config.always_include_port = true +end From b9891688878e12746b8d62fa17219854f373a213 Mon Sep 17 00:00:00 2001 From: Miles Gould Date: Sun, 2 Nov 2014 22:47:14 +0000 Subject: [PATCH 124/132] Fix "shared example group previously defined" warning We were getting the warning ``` WARNING: Shared example group 'crop suggest' has been previously defined at: /Users/miles/src/growstuff/spec/features/shared_examples/crop_suggest_spec.rb:3 ...and you are now defining it at: /Users/miles/src/growstuff/spec/features/shared_examples/crop_suggest_spec.rb:3 The new definition will overwrite the original one. ``` Following the suggestion at https://github.com/rspec/rspec-core/issues/828#issuecomment-38789977, I've renamed crop_suggest_spec.rb to crop_suggest.rb, which made the error go away without reducing the number of tests run. RSpec must have thought it was a spec file and loaded it directly, then loaded it again when it was first required by an actual spec file. --- .../shared_examples/{crop_suggest_spec.rb => crop_suggest.rb} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename spec/features/shared_examples/{crop_suggest_spec.rb => crop_suggest.rb} (100%) diff --git a/spec/features/shared_examples/crop_suggest_spec.rb b/spec/features/shared_examples/crop_suggest.rb similarity index 100% rename from spec/features/shared_examples/crop_suggest_spec.rb rename to spec/features/shared_examples/crop_suggest.rb From 2426c51951f0148a287fa41019623ef78b85cfb6 Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 11 Nov 2014 19:29:32 +1100 Subject: [PATCH 125/132] Fixed bug in deleting unused photos ... and wow, my tests were really not testing what I thought they were testing :-/ --- app/models/photo.rb | 2 +- spec/models/photo_spec.rb | 21 ++++++++++++++------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/app/models/photo.rb b/app/models/photo.rb index 32679eeed..862b9d28b 100644 --- a/app/models/photo.rb +++ b/app/models/photo.rb @@ -14,7 +14,7 @@ class Photo < ActiveRecord::Base # remove photos that aren't used by anything def destroy_if_unused - unless plantings.size > 0 and harvests.size > 0 + unless plantings.size > 0 or harvests.size > 0 self.destroy end end diff --git a/spec/models/photo_spec.rb b/spec/models/photo_spec.rb index 2a0b1d5ff..f17127301 100644 --- a/spec/models/photo_spec.rb +++ b/spec/models/photo_spec.rb @@ -39,16 +39,20 @@ describe Photo do expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound end - it 'they are no longer used by plantings' do + it 'they are used by plantings but not harvests' do + harvest.photos << photo planting.photos << photo - planting.destroy # photo is now no longer used by anything - expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + harvest.destroy # photo is now used by harvest but not planting + photo.destroy_if_unused + expect(lambda { photo.reload }).not_to raise_error ActiveRecord::RecordNotFound end - it 'they are no longer used by harvests' do + it 'they are used by harvests but not plantings' do harvest.photos << photo - harvest.destroy # photo is now no longer used by anything - expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound + planting.photos << photo + planting.destroy # photo is now used by harvest but not planting + photo.destroy_if_unused + expect(lambda { photo.reload }).not_to raise_error ActiveRecord::RecordNotFound end it 'they are no longer used by anything' do @@ -59,11 +63,14 @@ describe Photo do planting.destroy # photo is still used by harvest photo.reload - expect(photo).to be_an_instance_of Photo expect(photo.plantings.size).to eq 0 expect(photo.harvests.size).to eq 1 harvest.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 + photo.destroy_if_unused expect(lambda { photo.reload }).to raise_error ActiveRecord::RecordNotFound end From eb98910bea16d747b3fb5414dedf671b7d4c93eb Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 11 Nov 2014 19:45:07 +1100 Subject: [PATCH 126/132] Only send planting reminders to confirmed accounts --- lib/tasks/growstuff.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index c1043963d..4dd11bbcf 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -48,7 +48,7 @@ namespace :growstuff do every_n_weeks = 2 # send fortnightly if Date.today.cwday == send_on_day and Date.today.cw_week % every_n_weeks == 0 - Member.find_each do |m| + Member.confirmed.find_each do |m| Notifier.planting_reminder(m).deliver! end end From 50b8acc67cd51ca9ba131d56a473ef1ced163401 Mon Sep 17 00:00:00 2001 From: Taylor Griffin Date: Tue, 11 Nov 2014 19:52:31 +1100 Subject: [PATCH 127/132] put finished at bottom of list to make it more obvious when marking a planting finished --- app/views/plantings/_thumbnail.html.haml | 16 ++++++++-------- app/views/plantings/show.html.haml | 17 +++++++++-------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/app/views/plantings/_thumbnail.html.haml b/app/views/plantings/_thumbnail.html.haml index 4d2b6cdee..fd34b0703 100644 --- a/app/views/plantings/_thumbnail.html.haml +++ b/app/views/plantings/_thumbnail.html.haml @@ -17,14 +17,6 @@ in = link_to planting.location, planting.garden - - if planting.finished - %p - Finished: - - if planting.finished_at - = planting.finished_at - - else - Yes (no date specified) - %p - if planting.quantity Quantity: @@ -37,6 +29,14 @@ :growstuff_markdown #{ planting.description } + - if planting.finished + %p + Finished: + - if planting.finished_at + = planting.finished_at + - else + Yes (no date specified) + - if can? :edit, planting or can? :destroy, planting %p - if can? :edit, planting diff --git a/app/views/plantings/show.html.haml b/app/views/plantings/show.html.haml index cc3d58bae..72819f7ac 100644 --- a/app/views/plantings/show.html.haml +++ b/app/views/plantings/show.html.haml @@ -11,14 +11,6 @@ %b Planted: = @planting.planted_at ? @planting.planted_at : "not specified" - - if @planting.finished - %p - %b Finished: - - if @planting.finished_at - = @planting.finished_at - - else - Yes (no date specified) - %p %b Where: =link_to "#{@planting.owner}'s", @planting.owner @@ -39,6 +31,15 @@ %b Sun or shade? = @planting.sunniness + - if @planting.finished + %p + %b Finished: + - if @planting.finished_at + = @planting.finished_at + - else + Yes (no date specified) + + - if can? :edit, @planting or can? :destroy, @planting %p From 267c67125922dd7fbc5f3a5681c914d2871a4391 Mon Sep 17 00:00:00 2001 From: Skud Date: Tue, 11 Nov 2014 20:55:04 +1100 Subject: [PATCH 128/132] cweek not cw_week --- lib/tasks/growstuff.rake | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/tasks/growstuff.rake b/lib/tasks/growstuff.rake index 4dd11bbcf..0cf0a50f7 100644 --- a/lib/tasks/growstuff.rake +++ b/lib/tasks/growstuff.rake @@ -47,7 +47,7 @@ namespace :growstuff do send_on_day = 3 # wednesday every_n_weeks = 2 # send fortnightly - if Date.today.cwday == send_on_day and Date.today.cw_week % every_n_weeks == 0 + if Date.today.cwday == send_on_day and Date.today.cweek % every_n_weeks == 0 Member.confirmed.find_each do |m| Notifier.planting_reminder(m).deliver! end From caad748d081107203822ab2e041ea19cd57cf752 Mon Sep 17 00:00:00 2001 From: Yoong Kang Lim Date: Tue, 11 Nov 2014 20:54:16 +1100 Subject: [PATCH 129/132] Plantings index should display finished at date instead of planted at under the finished column. Added feature test for plantings index page. --- app/views/plantings/index.html.haml | 2 +- spec/features/plantings/planting_a_crop_spec.rb | 3 +++ spec/views/plantings/index.html.haml_spec.rb | 15 +++++++++++++-- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/app/views/plantings/index.html.haml b/app/views/plantings/index.html.haml index eb1e017dc..fe69ed344 100644 --- a/app/views/plantings/index.html.haml +++ b/app/views/plantings/index.html.haml @@ -43,7 +43,7 @@ %td= planting.planted_at %td - if planting.finished and planting.finished_at - = planting.planted_at + = planting.finished_at - elsif planting.finished Yes (no date specified) %td= planting.sunniness diff --git a/spec/features/plantings/planting_a_crop_spec.rb b/spec/features/plantings/planting_a_crop_spec.rb index ab120fa48..99114ab43 100644 --- a/spec/features/plantings/planting_a_crop_spec.rb +++ b/spec/features/plantings/planting_a_crop_spec.rb @@ -71,6 +71,9 @@ feature "Planting a crop", :js => true do end expect(page).to have_content "Planting was successfully created" expect(page).to have_content "Finished: August 30, 2014" + + visit plantings_path + expect(page).to have_content "August 30, 2014" end scenario "Marking a planting as finished without a date" do diff --git a/spec/views/plantings/index.html.haml_spec.rb b/spec/views/plantings/index.html.haml_spec.rb index d46b4e003..f80331117 100644 --- a/spec/views/plantings/index.html.haml_spec.rb +++ b/spec/views/plantings/index.html.haml_spec.rb @@ -8,8 +8,8 @@ describe "plantings/index" do @tomato = FactoryGirl.create(:tomato) @maize = FactoryGirl.create(:maize) page = 1 - per_page = 2 - total_entries = 2 + per_page = 3 + total_entries = 3 plantings = WillPaginate::Collection.create(page, per_page, total_entries) do |pager| pager.replace([ FactoryGirl.create(:planting, @@ -22,6 +22,13 @@ describe "plantings/index" do :crop => @maize, :description => '', :planted_at => Time.local(2013, 1, 13) + ), + FactoryGirl.create(:planting, + :garden => @garden, + :crop => @tomato, + :planted_at => Time.local(2013, 1, 13), + :finished_at => Time.local(2013, 1, 20), + :finished => true ) ]) end @@ -40,6 +47,10 @@ describe "plantings/index" do rendered.should contain 'January 13, 2013' end + it "displays finished time" do + rendered.should contain 'January 20, 2013' + end + it "provides data links" do render rendered.should contain "The data on this page is available in the following formats:" From 7023b4969bc3fa8a3e6a7ad9f31fbc74a99f8cf7 Mon Sep 17 00:00:00 2001 From: Kevin Rio Date: Tue, 11 Nov 2014 22:42:01 -0500 Subject: [PATCH 130/132] Update position of activity on member details page Switched activity and contact. --- app/views/members/show.html.haml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/views/members/show.html.haml b/app/views/members/show.html.haml index edf4d0b68..afb10d932 100644 --- a/app/views/members/show.html.haml +++ b/app/views/members/show.html.haml @@ -22,5 +22,5 @@ .col-md-3 = render :partial => "avatar", :locals => { :member => @member } = render :partial => "account", :locals => { :member => @member } - = render :partial => "contact", :locals => { :member => @member, :twitter_auth => @twitter_auth, :flickr_auth => @flickr_auth } = render :partial => "stats", :locals => { :member => @member } + = render :partial => "contact", :locals => { :member => @member, :twitter_auth => @twitter_auth, :flickr_auth => @flickr_auth } From b6d2a078b17a61d4e476141a4996e6faee5bda4c Mon Sep 17 00:00:00 2001 From: Kevin Rio Date: Tue, 11 Nov 2014 23:28:22 -0500 Subject: [PATCH 131/132] Adding to contributors list --- CONTRIBUTORS.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md index 650f8c321..d4e5085ee 100644 --- a/CONTRIBUTORS.md +++ b/CONTRIBUTORS.md @@ -48,3 +48,4 @@ submit the change with your pull request. - Maki Sugita / [macckii](https:://github.com/macckii) - Shiho Takagi / [oshiho3](https://github.com/oshiho3) - Emma Winston / [emmawinston](https://github.com/emmawinston) +- Kevin Rio / [krio](https://github.com/krio) From bfce66a8f7eea8050a7be029053f1d54959157a2 Mon Sep 17 00:00:00 2001 From: Skud Date: Thu, 13 Nov 2014 11:28:14 +1100 Subject: [PATCH 132/132] Increase devise timeout to 30 days --- config/initializers/devise.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb index eb51fab96..801a6677f 100644 --- a/config/initializers/devise.rb +++ b/config/initializers/devise.rb @@ -126,8 +126,8 @@ Devise.setup do |config| # ==> Configuration for :timeoutable # The time you want to timeout the user session without activity. After this # time the user will be asked for credentials again. Default is 30 minutes. - # config.timeout_in = 30.minutes - + config.timeout_in = 30.days + # If true, expires auth token on session timeout. # config.expire_auth_token_on_timeout = false